diff --git a/docs/howto.md b/docs/howto.md
index 3ff18dde75..232e64edae 100644
--- a/docs/howto.md
+++ b/docs/howto.md
@@ -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.
+
+
+## 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
diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java
index ee92f03890..e4e0d3c334 100644
--- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java
@@ -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
diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml
index 1cd27087eb..79a2ed601f 100644
--- a/spring-boot-autoconfigure/pom.xml
+++ b/spring-boot-autoconfigure/pom.xml
@@ -21,6 +21,11 @@
${project.version}
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ true
+
commons-dbcp
commons-dbcp
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/MessageConverters.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/MessageConverters.java
new file mode 100644
index 0000000000..1f7f5374bd
--- /dev/null
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/MessageConverters.java
@@ -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> defaults;
+
+ private List> overrides;
+
+ private Object lock = new Object();
+
+ private List> converters;
+
+ public MessageConverters() {
+ this(Collections.> emptyList());
+ }
+
+ public MessageConverters(Collection> overrides) {
+ this.overrides = new ArrayList>(overrides);
+ }
+
+ public List> getMessageConverters() {
+ if (this.converters == null) {
+ synchronized (this.lock) {
+ if (this.converters == null) {
+ this.converters = new ArrayList>();
+ 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> getDefaultMessageConverters() {
+ if (this.defaults == null) {
+ synchronized (this.lock) {
+ if (this.defaults == null) {
+ this.defaults = new ArrayList>();
+ this.defaults.addAll(new WebMvcConfigurationSupport() {
+ public List> defaultMessageConverters() {
+ return super.getMessageConverters();
+ }
+ }.defaultMessageConverters());
+ List> xmls = new ArrayList>();
+ 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);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java
index 622b2722d4..98c5b51944 100644
--- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java
@@ -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> converters = Collections.emptyList();
+
+ @Bean
+ @ConditionalOnMissingBean
+ public MessageConverters messageConverters() {
+ List> converters = new ArrayList>(
+ 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> 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 mappers = BeanFactoryUtils
+ .beansOfTypeIncludingAncestors(this.beanFactory,
+ ObjectMapper.class).values();
+ Collection 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() {
diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfigurationTests.java
index 6df71f80ee..4a18095816 100644
--- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfigurationTests.java
+++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfigurationTests.java
@@ -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
diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java
index fa0fb09782..4f6a03635c 100644
--- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java
+++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java
@@ -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) 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> 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() {
+
+ @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 {