Search code examples
javaspringspring-bootjacksonspring-web

Duplicate MessageConverters in RestTemplateBuilder?


I'm using spring-boot with spring-web and jackson.

Problem: when a RestTemplate is initialized automatically by spring, the constructor receives some duplicate MessageConverters:

org.springframework.http.converter.ByteArrayHttpMessageConverter@6a1b4854,
org.springframework.http.converter.StringHttpMessageConverter@2d5b549b, 
org.springframework.http.converter.StringHttpMessageConverter@6a175162, 
org.springframework.http.converter.ResourceHttpMessageConverter@7641c4e7, 
org.springframework.http.converter.ResourceRegionHttpMessageConverter@650a0b50, 
org.springframework.http.converter.xml.SourceHttpMessageConverter@55e3b64d, 
org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@52f71d2, 
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@f3c27e9, 
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@7d31fb6c, 
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@701c413, 
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@48543f11

You see, there are 3 duplicates:

StringHttpMessageConverter
MappingJackson2HttpMessageConverter
MappingJackson2XmlHttpMessageConverter

As I don't initialize any message converters myself: why does the application context contain duplicate converters at all, that are then added to the resttemplate?

Especially: doesn't this confuse the (de)serializing if some converters occur duplicate (but with different configuration)?

For example: the ObjectMapper of the first MappingJackson2HttpMessageConverter contains more registeredModuleTypes [Jdk8Module, JavaTimeModule, ParamterNamesModule, JsonComponentModule, GeoModule] than the 2nd one (that only contains: [Jdk8Module, JavaTimeModule]).

Does that make sense?

It's instantiated via RestTemplateAutoConfiguration.restTemplateBuilder(), there all the duplicate MessageConverters are already present.


Solution

  • The culprit is here, at HttpMessageConverters

    public HttpMessageConverters(boolean addDefaultConverters,
            Collection<HttpMessageConverter<?>> converters) {
        List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
                addDefaultConverters ? getDefaultConverters() : Collections.emptyList());
        combined = postProcessConverters(combined);
        this.converters = Collections.unmodifiableList(combined);
    }
    

    Specifically, this line (formatted)

    List<HttpMessageConverter<?>> combined = 
           getCombinedConverters(
               converters, 
               addDefaultConverters 
                   ? getDefaultConverters() 
                   : Collections.emptyList());
    

    The converters collection contains the scanned HttpMessageConverter(s).
    Based on the environment.

    enter image description here

    That list is then joined with a default one provided by WebMvcConfigurationSupport

    enter image description here

    public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
        private static final boolean romePresent;
        private static final boolean jaxb2Present;
        private static final boolean jackson2Present;
        private static final boolean jackson2XmlPresent;
        private static final boolean jackson2SmilePresent;
        private static final boolean jackson2CborPresent;
        private static final boolean gsonPresent;
        private static final boolean jsonbPresent;
        ...
    

    Infact the documentations for WebMvcConfigurationSupport states

    This class registers ... ... a range of HttpMessageConverters depending on the third-party libraries available on the classpath.

    The scanned HttpMessageConverter(s) are found and instantiated via HttpMessageConvertersAutoConfiguration, whose documentation is

    Auto-configuration for HttpMessageConverters.

    That class exposes by itself a StringHttpMessageConverter

    @Bean
    @ConditionalOnMissingBean
    public StringHttpMessageConverter stringHttpMessageConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(
                this.properties.getCharset());
        converter.setWriteAcceptCharset(false);
        return converter;
    }
    

    Than, it imports Jackson or Gson auto-configurations

    @Import({ 
        JacksonHttpMessageConvertersConfiguration.class
        GsonHttpMessageConvertersConfiguration.class,
        JsonbHttpMessageConvertersConfiguration.class 
    })
    

    And that's how those environment-based ones are "summed" to the pre-defined ones.


    Spring doesn't get confused by duplicates because it just takes the first which is compatible.
    See how an HttpMessageConverter is choosed

    enter image description here

    You can see it is just a simple for loop, and each convert is asked to say "can I do this?" via the canWrite method

    enter image description here

    The first valid is picked.