Support for @Beans of type HttpMessageConverter, and Jackson specific details
You can contribute additional HttpMessageConverters by simply adding beans of that type in a Spring Boot context. If a bean you add is of a type that would have been included by default anyway (like MappingJackson2HttpMessageConverter for JSON conversions) then it will replace the default value. A convenience bean is provided of type MessageConverters (always available if you use the default MVC configuration) which has some useful methods to access the default and user-enhanced message converters (useful, for example if you want to manually inject them into a custom RestTemplate). There are also some convenient configuration shortcuts for Jackson2. The smallest change that might work is to just add beans of type Module to your context. They will be registered with the default ObjectMapper and then injected into the default message converter. In addition, if your context contains any beans of type ObjectMapper then all of the Module beans will be registered with all of the mappers.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
|
||||
|
||||
/**
|
||||
* Convenient utility for adding and merging additional {@link HttpMessageConverter} in an
|
||||
* application context. It also modifies the default converters a bit (putting XML
|
||||
* converters at the back of the list if they are present).
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class MessageConverters {
|
||||
|
||||
private List<HttpMessageConverter<?>> defaults;
|
||||
|
||||
private List<HttpMessageConverter<?>> overrides;
|
||||
|
||||
private Object lock = new Object();
|
||||
|
||||
private List<HttpMessageConverter<?>> converters;
|
||||
|
||||
public MessageConverters() {
|
||||
this(Collections.<HttpMessageConverter<?>> emptyList());
|
||||
}
|
||||
|
||||
public MessageConverters(Collection<HttpMessageConverter<?>> overrides) {
|
||||
this.overrides = new ArrayList<HttpMessageConverter<?>>(overrides);
|
||||
}
|
||||
|
||||
public List<HttpMessageConverter<?>> getMessageConverters() {
|
||||
if (this.converters == null) {
|
||||
synchronized (this.lock) {
|
||||
if (this.converters == null) {
|
||||
this.converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
getDefaultMessageConverters(); // ensure they are available
|
||||
for (HttpMessageConverter<?> fallback : this.defaults) {
|
||||
boolean overridden = false;
|
||||
for (HttpMessageConverter<?> converter : this.overrides) {
|
||||
if (fallback.getClass()
|
||||
.isAssignableFrom(converter.getClass())) {
|
||||
if (!this.converters.contains(converter)) {
|
||||
this.converters.add(converter);
|
||||
overridden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!overridden) {
|
||||
this.converters.add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.converters;
|
||||
}
|
||||
|
||||
public List<HttpMessageConverter<?>> getDefaultMessageConverters() {
|
||||
if (this.defaults == null) {
|
||||
synchronized (this.lock) {
|
||||
if (this.defaults == null) {
|
||||
this.defaults = new ArrayList<HttpMessageConverter<?>>();
|
||||
this.defaults.addAll(new WebMvcConfigurationSupport() {
|
||||
public List<HttpMessageConverter<?>> defaultMessageConverters() {
|
||||
return super.getMessageConverters();
|
||||
}
|
||||
}.defaultMessageConverters());
|
||||
List<HttpMessageConverter<?>> xmls = new ArrayList<HttpMessageConverter<?>>();
|
||||
for (HttpMessageConverter<?> converter : this.defaults) {
|
||||
// Shift XML converters to the back of the list so they only get
|
||||
// used if nothing else works...
|
||||
if (converter instanceof AbstractXmlHttpMessageConverter) {
|
||||
xmls.add(converter);
|
||||
}
|
||||
}
|
||||
this.defaults.removeAll(xmls);
|
||||
this.defaults.addAll(xmls);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(this.defaults);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,15 +17,19 @@
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.servlet.Servlet;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -37,6 +41,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -46,6 +51,8 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.context.request.RequestContextListener;
|
||||
import org.springframework.web.filter.HiddenHttpMethodFilter;
|
||||
@@ -62,6 +69,9 @@ import org.springframework.web.servlet.view.BeanNameViewResolver;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}.
|
||||
*
|
||||
@@ -101,6 +111,17 @@ public class WebMvcAutoConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<HttpMessageConverter<?>> converters = Collections.emptyList();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MessageConverters messageConverters() {
|
||||
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(
|
||||
this.converters);
|
||||
return new MessageConverters(converters);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
|
||||
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
|
||||
@@ -126,6 +147,53 @@ public class WebMvcAutoConfiguration {
|
||||
@Autowired
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
@Autowired
|
||||
private MessageConverters messageConverters;
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.addAll(this.messageConverters.getMessageConverters());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnBean(ObjectMapper.class)
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
protected static class ObjectMappers {
|
||||
|
||||
@Autowired
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Collection<ObjectMapper> mappers = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory,
|
||||
ObjectMapper.class).values();
|
||||
Collection<Module> modules = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, Module.class)
|
||||
.values();
|
||||
for (ObjectMapper mapper : mappers) {
|
||||
mapper.registerModules(modules);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Primary
|
||||
public ObjectMapper jacksonObjectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
|
||||
ObjectMapper objectMapper) {
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
|
||||
converter.setObjectMapper(objectMapper);
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(InternalResourceViewResolver.class)
|
||||
public InternalResourceViewResolver defaultViewResolver() {
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link JmxAutoConfiguration}
|
||||
@@ -66,7 +65,6 @@ public class JmxAutoConfigurationTests {
|
||||
this.context.refresh();
|
||||
|
||||
this.context.getBean(MBeanExporter.class);
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -28,14 +29,17 @@ import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
|
||||
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor;
|
||||
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
|
||||
import org.springframework.boot.context.embedded.MockEmbeddedServletContainerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.servlet.HandlerAdapter;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
@@ -43,12 +47,26 @@ import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebMvcAutoConfiguration}.
|
||||
@@ -86,6 +104,8 @@ public class WebMvcAutoConfigurationTests {
|
||||
this.context.register(Config.class, WebMvcAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertEquals(6, this.context.getBeanNamesForType(HandlerMapping.class).length);
|
||||
assertFalse(this.context.getBean(RequestMappingHandlerAdapter.class)
|
||||
.getMessageConverters().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,6 +143,46 @@ public class WebMvcAutoConfigurationTests {
|
||||
equalTo((Resource) new ClassPathResource("/foo/")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customJacksonConverter() throws Exception {
|
||||
this.context = new AnnotationConfigEmbeddedWebApplicationContext();
|
||||
this.context.register(JacksonConfig.class, Config.class,
|
||||
WebMvcAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
MappingJackson2HttpMessageConverter converter = this.context
|
||||
.getBean(MappingJackson2HttpMessageConverter.class);
|
||||
assertEquals(this.context.getBean(ObjectMapper.class),
|
||||
converter.getObjectMapper());
|
||||
MessageConverters converters = this.context.getBean(MessageConverters.class);
|
||||
assertTrue(converters.getMessageConverters().contains(converter));
|
||||
assertEquals(converters.getMessageConverters(),
|
||||
this.context.getBean(RequestMappingHandlerAdapter.class)
|
||||
.getMessageConverters());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customJacksonModules() throws Exception {
|
||||
this.context = new AnnotationConfigEmbeddedWebApplicationContext();
|
||||
this.context.register(ModulesConfig.class, Config.class,
|
||||
WebMvcAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
|
||||
@SuppressWarnings({ "unchecked", "unused" })
|
||||
ObjectMapper result = verify(mapper).registerModules(
|
||||
(Iterable<Module>) argThat(hasItem(this.context.getBean(Module.class))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doubleModuleRegistration() throws Exception {
|
||||
this.context = new AnnotationConfigEmbeddedWebApplicationContext();
|
||||
this.context.register(DoubleModulesConfig.class, Config.class,
|
||||
WebMvcAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
|
||||
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, List<Resource>> getMappingLocations()
|
||||
throws IllegalAccessException {
|
||||
@@ -180,6 +240,91 @@ public class WebMvcAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class ModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
return new SimpleModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
return Mockito.mock(ObjectMapper.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class DoubleModulesConfig {
|
||||
|
||||
@Bean
|
||||
public Module jacksonModule() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Foo.class, new JsonSerializer<Foo>() {
|
||||
|
||||
@Override
|
||||
public void serialize(Foo value, JsonGenerator jgen,
|
||||
SerializerProvider provider) throws IOException,
|
||||
JsonProcessingException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("foo", "bar");
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
});
|
||||
return module;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(jacksonModule());
|
||||
return mapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class Foo {
|
||||
|
||||
private String name;
|
||||
|
||||
private Foo() {
|
||||
|
||||
}
|
||||
|
||||
static Foo create() {
|
||||
return new Foo();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public MappingJackson2HttpMessageConverter jacksonMessaegConverter() {
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
|
||||
converter.setObjectMapper(objectMapper());
|
||||
return converter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class Config {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user