Since one year or so, I try to show the developer community that there’s no magic involved in Spring Boot but rather just straightforward software engineering. This is achieved with blog posts and conference talks. At jDays, Stéphane Nicoll was nice enough to attend my talk and pointed out an issue in the code. I didn’t fix it then, and it came back to bite me last week during a Pivotal webinar. Since a lesson learned is only as useful as its audience, I’d like to share my mistake with the world, or at least with you, dear readers.
The context is that of a Spring Boot starter for the XStream library:
XStream is a simple library to serialize objects to XML and back again.
- (De)serialization capabilities are implemented as instance methods of the
XStream
class - Customization of the (de)serialization process is implemented through converters registered in the
XStream
instance
The goal of the starter is to ease the usage of the library. For example, it creates an XStream instance in the context if there’s none already:
@Configuration
public class XStreamAutoConfiguration {
@Bean
@ConditionalOnMissingBean(XStream.class)
public XStream xStream() {
return new XStream();
}
}
Also, it will collect all custom converters from the context and register them in the existing instance:
@Configuration
public class XStreamAutoConfiguration {
@Bean
@ConditionalOnMissingBean(XStream.class)
public XStream xStream() {
return new XStream();
}
@Bean
public Collection<Converter> converters(XStream xstream, Collection<Converter> converters) {
converters.forEach(xstream::registerConverter);
return converters;
}
}
The previous snippet achieves the objective, as Spring obediently inject both xstream
and converters
dependency beans in the method.
Yet, a problem happened during the webinar demo:
can you spot it?
If you answered about an extra converters
bean registered into the context, you are right.
But the issue occurs if there’s another converters
bean registered by the client code, and it’s of different type i.e. not a Collection
.
Hence, registration of converters must not happen in beans methods, but only in a @PostConstruct
one.
- Option 1
The first option is to convert the
@Bean
to@PostConstruct
.@Configuration public class XStreamAutoConfiguration { @Bean @ConditionalOnMissingBean(XStream.class) public XStream xStream() { return new XStream(); } @PostConstruct public void register(XStream xstream, Collection<Converter> converters) { converters.forEach(xstream::registerConverter); } }
Unfortunately,
@PostConstruct
doesn’t allow for methods to have arguments. The code doesn’t compile. - Option 2
The alternative is to inject both beans into attributes of the configuration class, and use them in the
@PostConstruct
-annotated method.@Configuration public class XStreamAutoConfiguration { @Autowired private XStream xstream; @Autowired private Collection<Converter> converters; @Bean @ConditionalOnMissingBean(XStream.class) public XStream xStream() { return new XStream(); } @PostConstruct public void register() { converters.forEach(xstream::registerConverter); } }
This compiles fine, but Spring enters a cycle trying both to inject the
XStream
instance into the configuration and to create it as a bean at the same time. - Option 3
The final (and only valid) option is to learn from the master and use another configuration class - a nested one. Looking at Spring Boot code source, it’s obviously a pattern.
The final code looks like this:
@Configuration public class XStreamAutoConfiguration { @Bean @ConditionalOnMissingBean(XStream.class) public XStream xStream() { return new XStream(); } @Configuration public static class XStreamConverterAutoConfiguration { @Autowired private XStream xstream; @Autowired private Collection<Converter> converters; @PostConstruct public void registerConverters() { converters.forEach(converter -> xstream.registerConverter(converter)); } } }