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:
Dave Syer 2013-12-14 21:53:06 +00:00
parent 2b16a4af39
commit c2b499c775
7 changed files with 411 additions and 4 deletions

View File

@ -40,7 +40,88 @@ Javadocs. Some rules of thumb:
* Look for `@ConditionalOnExpression` annotations that switch features
on and off in response to SpEL expressions, normally evaluated with
placeholders resolved from the `Environment`.
## Write a JSON REST Service
Any Spring `@RestController` in a Spring Boot application should
render JSON response by default as long as Jackson2 is on the
classpath. For example:
```java
@RestController
public class MyController {
@RequestMapping("/thing")
public MyThing thing() {
return new MyThing();
}
}
```
As long as `MyThing` can be serialized by Jackson2 (e.g. a normal POJO
or Groovy object) then `http://localhost:8080/thing` will serve a JSON
representation of it by default. Sometimes in a browser you might see
XML responses (but by default only if `MyThing` was a JAXB object)
because browsers tend to send accept headers that prefer XML.
## Customize the Jackson ObjectMapper
Spring MVC (client and server side) uses `HttpMessageConverters` to
negotiate content conversion in an HTTP exchange. If Jackson is on the
classpath you already get a default converter with a vanilla
`ObjectMapper`. Spring Boot has some features to make it easier to
customize this behaviour.
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. To replace the default `ObjectMapper` completely, define a
`@Bean` of that type and mark it as `@Primary`.
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. So there is a global mechanism for contributing custom
modules when you add new features to your application.
Finally, if you provide any `@Beans` of type
`MappingJackson2HttpMessageConverter` then they will replace the
default value in the MVC configuration. Also, 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.
See also the [section on `HttpMessageConverters`](#message.converters)
and the `WebMvcAutoConfiguration` source code for more details.
<span id="message.converters"/>
## Customize the @ResponseBody Rendering
Spring uses `HttpMessageConverters` to render `@ResponseBody` (or
responses from `@RestControllers`). You can contribute additional
converters 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`).
As in normal MVC usage, any `WebMvcConfigurerAdapter` beans that you
provide can also contribute converters by overriding the
`configureMessageConverters` method, but unlike with normal MVC, you
can supply only additional converters that you need (because Spring
Boot uses the same mechanism to contribute its defaults). Finally, if
you opt out of the Spring Boot default MVC configuration by providing
your own `@EnableWebMvc` configuration, then you can take control
completely and do everything manually using `getMessageConverters`
from `WebMvcConfigurationSupport`.
See the `WebMvcAutoConfiguration` source code for more details.
## Add a Servlet, Filter or ServletContextListener to an Application
`Servlet`, `Filter`, `ServletContextListener` and the other listeners

View File

@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.MessageConverters;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
@ -92,8 +93,11 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
@Bean
@ConditionalOnMissingBean
public EndpointHandlerAdapter endpointHandlerAdapter() {
return new EndpointHandlerAdapter();
public EndpointHandlerAdapter endpointHandlerAdapter(
final MessageConverters messageConverters) {
EndpointHandlerAdapter adapter = new EndpointHandlerAdapter();
adapter.setMessageConverters(messageConverters.getMessageConverters());
return adapter;
}
@Override

View File

@ -21,6 +21,11 @@
<version>${project.version}</version>
</dependency>
<!-- Optional -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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 {