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.
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 theSomeProperties
example above issome.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.