Reputation: 38526
I am trying to write up an integration test case with Spring Boot Test.
I customize the ConversionService
to know about the new java.time
types:
@Configuration
public class ConversionServiceConfiguration {
@Bean
public static ConversionService conversionService() {
final FormattingConversionService reg = new DefaultFormattingConversionService();
new DateTimeFormatterRegistrar().registerFormatters(reg);
return reg;
}
}
and then later expect it to work:
@Component
class MyServiceConfig {
@Value("${max-watch-time:PT20s}")
private Duration maxWatchTime = Duration.ofSeconds(20);
}
When running under the normal SpringApplication.run
this seems to work fine. However, in my test case:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, classes= {
MyServiceMain.class,
AttachClientRule.class
})
public class MyTest {
@Inject
@Rule
public AttachClientRule client;
@Test(expected=IllegalArgumentException.class)
public void testBad() throws Exception {
client.doSomethingIllegal();
}
}
it blows up:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'AttachClientRule': Unsatisfied dependency expressed through constructor parameter 0:
Error creating bean with name 'MyServiceConfig': Unsatisfied dependency expressed through field 'maxWatchTime': Failed to convert value of type [java.lang.String] to required type [java.time.Duration];
nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [java.time.Duration]: no matching editors or conversion strategy found;
Peering deep into the guts of the TypeConverterDelegate
that does the actual conversion, it seems to capture the ConversionService
used from a field on the DefaultListableBeanFactory
. Setting a watchpoint on where that field is set, I find the AbstractApplicationContext.refresh()
method:
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh(); // <--- MyServiceConfig initialized here
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory); // <--- DefaultListableBeanFactory.conversionService set here!!!
// Last step: publish corresponding event.
finishRefresh();
So the @Value
injection is happening before the ConversionService
is applied to the BeanFactory
. No bueno!
I've found what seems to be a workaround:
@Configuration
public class ConversionServiceConfiguration implements BeanFactoryPostProcessor {
@Bean
public static ConversionService conversionService() {
final FormattingConversionService reg = new DefaultFormattingConversionService();
new DateTimeFormatterRegistrar().registerFormatters(reg);
return reg;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.setConversionService(conversionService());
}
}
This forces the initialization to happen earlier on, but doesn't feel like the right solution (at least it's not documented as such).
Where have I gone wrong? Spring 4.3.0, Spring Boot 1.4.0M3
EDIT
And now I've discovered another way for it to fail! Without making the same configuration class implement EnvironmentAware
:
@Override
public void setEnvironment(Environment environment) {
((AbstractEnvironment) environment).setConversionService(conversionService());
}
I find that the PropertySourcesPropertyResolver
uses the wrong (default) ConversionService
. This is driving me mad!
Caused by: java.lang.IllegalArgumentException: Cannot convert value [PT15s] from source type [String] to target type [Duration] at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:94) at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:65) at org.springframework.core.env.AbstractPropertyResolver.getProperty(AbstractPropertyResolver.java:143) at org.springframework.core.env.AbstractEnvironment.getProperty(AbstractEnvironment.java:546) at com.mycorp.DoSomething.go(DoSomething.java:103)
Upvotes: 6
Views: 4475
Reputation: 38526
The Spring Boot developers have confirmed that this is poorly documented and does not work as specified: https://github.com/spring-projects/spring-boot/issues/6222
Upvotes: 2
Reputation: 24561
Try to remove static
keyword from conversionService
bean definition.
Upvotes: 0