Spring Boot logo

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.
  1. (De)serialization capabilities are implemented as instance methods of the XStream class
  2. 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));
            }
        }
    }

The fixed code is available on Github. And grab the webinar while it’s hot.