javaspringspring-bootspring-mvcspring-expression-language

How to reference bean that is a record from a Spring expression?


I have Spring Boot automatically reading in some application.yaml configuration into this record:

package foo.bar.baz;

@ConfigurationProperties(prefix = "apns")
public record ApnsConfiguration(
    Duration gracefulShutdownTimeout,
    int maxAttempts,
    Duration minDelay,
    Duration maxDelay,
    List<ApnsEndpoint> endpoints

This object is read in perfectly fine and it is used with no problems in a number of places.

I'm now trying to use @Retryable to retry a method. When I specify literals for maxAttempt and for the arguments to @Backoff it works fine. But I want to get those values from the bean (the multiplier is a constant as we are not interested in varying it in configuration).

I have tried:

    @Retryable(
        retryFor = RetryableException.class,
        maxAttemptsExpression = "#{@apnsConfiguration.maxAttempts}",
        backoff = @Backoff(
            delayExpression =
                "#{apnsConfiguration.minDelay.toMillis}",
            maxDelayExpression =
                "#{@apnsConfiguration.maxDelay.toMillis}",
            multiplier = BACKOFF_MULTIPLIER)
    )

However when do that tests (and startup) fails with these errors:

EL1058E: A problem occurred when trying to resolve bean 'apnsConfiguration': 'Could not resolve bean reference against BeanFactory'
org.springframework.expression.spel.SpelEvaluationException: EL1058E: A problem occurred when trying to resolve bean 'apnsConfiguration': 'Could not resolve bean reference against BeanFactory'
[snip]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'apnsConfiguration' available
    at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:925)

I didn't understand why it wasn't finding the bean so I printed out the names of all the beans using this:

    @EventListener
    public void handleContextRefreshed(final ContextRefreshedEvent event) {
        Arrays.stream(event.getApplicationContext().getBeanDefinitionNames())
            .forEach(name -> log.info("Bean name: {}", name));
    }

When I look at that output I see this:

Bean name: baz-foo.bar.baz.ApnsConfiguration

So I next changed the Spring expressions to be stuff like:

maxAttemptsExpression = "#{@baz-foo.bar.baz.ApnsConfiguration.maxAttempts}"

But that also failed to find the bean, complaining it couldn't find a bean named baz.

In that printout of beans I see other beans I've created with totally normal names. For example I have a

@Component
public final class ApnsMessageConverter implements Converter<........>

and that shows up in the list of names as apnsMessageConverter as expected.

So it appears that Spring generates weird names when you have a record be a bean? I though about giving it an explicit name by putting something like @Configuration on the ApnsConfiguration record since @Configuration allows you to specify a name but it's not an allowable annotation there.

So is there any way to force a record to have a specific bean name? And if not, does SpringEL have some syntax which will take baz-foo.bar.baz.ApnsConfiguration as the name of the bean rather than thinking that baz is the name of the bean (and I guess thinking the rest of the string is part of some expression)?


UPDATE

Through trial and error I have discovered that this works:

maxAttemptsExpression = "#{(@'baz-foo.bar.baz..ApnsConfiguration').maxAttempts}"

So at least I have a solution.

But that's ugly as sin so I'd really prefer to somehow give the bean a better name.


Solution

  • I did some digging and found this github issue Allow specifying beanname on @EnableConfigurationProperties

    As you have found out is that ConfigurationProperties are registered using their FQCN (fully qualified class name) than just their class name.

    it is also documented here in the official docs

    When the @ConfigurationProperties bean is registered using configuration property scanning or > through @EnableConfigurationProperties, the bean has a conventional name: <prefix>-<fqn>, where > <prefix> is the environment key prefix specified in the @ConfigurationProperties annotation and > <fqn> is the fully qualified name of the bean. If the annotation does not provide any prefix, only the > fully qualified name of the bean is used.

    Assuming that it is in the com.example.app package, the bean > name of the SomeProperties example above is some.properties-com.example.app.SomeProperties.

    Sadly the recommended course of action is to either add a @Component annotation to the ConfigurationProperties annotated class, or an @Bean annotation function in a configuration class where you manually instantiate your properties class.

    I couldn't really understand the reasoning, but it seems that Spring considers properties files to not be in a public API which means they can change whenever, and since spel expressions are a runtime thing they dont recommend using dynamic values from external sources as it can be fragile.

    Thats my interpretation, so dont take my word for it.