I\'m trying to create a converter for a custom media-type like application/vnd.custom.hal+json
. I saw this answer here, but it won\'t work since you don\'t have
Not sure when this was fixed, but as of 1.1.8.RELEASE
, this problem no-longer exists since it is using ClassUtils.isAssignableValue
. Leaving the original answer here just for information.
There seem to be multiple issues at play here, so I'm going to summarize my findings as the answer. I still don't really have a solution for what I'm trying to do, but I'm going to talk to the Spring Boot folks to see if what's happening is intended or not.
MappingJackson2HttpMessageConverter
?This applies to version 1.1.4.RELEASE
of Spring Boot; I haven't checked other versions. The constructor of the HttpMessageConverters
class is as follows:
/**
* Create a new {@link HttpMessageConverters} instance with the specified additional
* converters.
* @param additionalConverters additional converters to be added. New converters will
* be added to the front of the list, overrides will replace existing items without
* changing the order. The {@link #getConverters()} methods can be used for further
* converter manipulation.
*/
public HttpMessageConverters(Collection<HttpMessageConverter<?>> additionalConverters) {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
List<HttpMessageConverter<?>> defaultConverters = getDefaultConverters();
for (HttpMessageConverter<?> converter : additionalConverters) {
int defaultConverterIndex = indexOfItemClass(defaultConverters, converter);
if (defaultConverterIndex == -1) {
converters.add(converter);
}
else {
defaultConverters.set(defaultConverterIndex, converter);
}
}
converters.addAll(defaultConverters);
this.converters = Collections.unmodifiableList(converters);
}
Inside the for
loop. Notice that it determines an index in the list by calling the indexOfItemClass
method. That method looks like this:
private <E> int indexOfItemClass(List<E> list, E item) {
Class<? extends Object> itemClass = item.getClass();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getClass().isAssignableFrom(itemClass)) {
return i;
}
}
return -1;
}
Since my class extends MappingJackson2HttpMessageConverter
the if
statement returns true
. This means that in the constructor, we have a valid index. Spring Boot then replaces the existing instance with the new one, which is exactly what I am seeing.
I don't know. It doesn't seem to be and seems very strange to me.
Sort of. See here. It says:
Any
HttpMessageConverter bean
that is present in the context will be added to the list of converters. You can also override default converters that way.
However, overriding a converter simply because it is a subtype of an existing one doesn't seem like helpful behavior.
Spring HATEOAS' lifecycle is separate from Spring Boot. Spring HATEOAS registers its handler for the application/hal+json
media-type in the HyperMediaSupportBeanDefinitionRegistrar
class. The relevant method is:
private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter;
ObjectMapper objectMapper = halConverterCandidate.getObjectMapper();
if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) {
return converters;
}
}
}
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
MappingJackson2HttpMessageConverter halConverter = new MappingJackson2HttpMessageConverter();
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); //HAL_JSON is just a MediaType instance for application/hal+json
halConverter.setObjectMapper(halObjectMapper);
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
result.add(halConverter);
result.addAll(converters);
return result;
}
The converters
argument is passed-in via this snippet from the postProcessBeforeInitialization
method from the same class. Relevant snippet is:
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters()));
}
MappingJackson2HttpMessageConverter
?I'm not sure. Sub-classing ExtensibleMappingJackson2HttpMessageConverter<T>
(shown in the question) works for the time being. Another option would perhaps be to create a private instance of MappingJackson2HttpMessageConverter
inside your custom converter, and simply delegate to that. Either way, I am going to open an issue with the Spring Boot project and get some feedback from them. I'll then update with answer with any new information.
Spring boot docs explicitly states that adding a custom MappingJackson2HttpMessageConverter
replaces the default value.
From docs:
Finally, if you provide any
@Beans
of typeMappingJackson2HttpMessageConverter
then they will replace the default value in the MVC configuration. Also, a convenience bean is provided of typeHttpMessageConverters
(always available if you use the default MVC configuration) which has some useful methods to access the default and user-enhanced message converters.