Skip to content

Commit 81d7f5b

Browse files
committed
SPR-6375 - Register sensible default HTTP Message Converters based on what is available in the classpath
1 parent 666700f commit 81d7f5b

File tree

3 files changed

+117
-54
lines changed

3 files changed

+117
-54
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,71 @@
1616

1717
package org.springframework.web.servlet.config;
1818

19+
import org.w3c.dom.Element;
20+
1921
import org.springframework.beans.factory.config.BeanDefinition;
2022
import org.springframework.beans.factory.config.RuntimeBeanReference;
2123
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
2224
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
25+
import org.springframework.beans.factory.support.ManagedList;
2326
import org.springframework.beans.factory.support.RootBeanDefinition;
2427
import org.springframework.beans.factory.xml.BeanDefinitionParser;
2528
import org.springframework.beans.factory.xml.ParserContext;
2629
import org.springframework.core.convert.ConversionService;
2730
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
31+
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
32+
import org.springframework.http.converter.FormHttpMessageConverter;
33+
import org.springframework.http.converter.StringHttpMessageConverter;
34+
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
35+
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
36+
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
2837
import org.springframework.util.ClassUtils;
2938
import org.springframework.validation.Validator;
3039
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
3140
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
3241
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
3342
import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping;
34-
import org.w3c.dom.Element;
3543

3644
/**
37-
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses the {@code annotation-driven} element to configure
38-
* a Spring MVC web application.
39-
* <p>
40-
* Responsible for:
45+
* {@link BeanDefinitionParser} that parses the {@code annotation-driven} element to configure a Spring MVC web
46+
* application.
47+
*
48+
* <p>Responsible for:
4149
* <ol>
42-
* <li>Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods using @RequestMapping annotations.
50+
* <li>Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods
51+
* using @RequestMapping annotations.
4352
* <li>Registering a AnnotationMethodHandlerAdapter bean for invoking annotated @Controller methods.
44-
* Will configure the HandlerAdapter's <code>webBindingInitializer</code> property for centrally configuring @Controller DataBinder instances:
53+
* Will configure the HandlerAdapter's <code>webBindingInitializer</code> property for centrally configuring
54+
* {@code @Controller} {@code DataBinder} instances:
4555
* <ul>
46-
* <li>Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance created by the default {@link FormattingConversionServiceFactoryBean}.
47-
* <li>Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the default {@link LocalValidatorFactoryBean} <i>if the JSR-303 API is present in the classpath.
56+
* <li>Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance
57+
* created by the default {@link FormattingConversionServiceFactoryBean}.
58+
* <li>Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the
59+
* default {@link LocalValidatorFactoryBean} <em>if the JSR-303 API is present on the classpath</em>.
60+
* <li>Configures standard {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters},
61+
* including the {@link Jaxb2RootElementHttpMessageConverter} <em>if JAXB2 is present on the classpath</em>, and
62+
* the {@link MappingJacksonHttpMessageConverter} <em>if Jackson is present on the classpath</em>.
4863
* </ul>
4964
* </ol>
5065
*
5166
* @author Keith Donald
5267
* @author Juergen Hoeller
68+
* @author Arjen Poutsma
5369
* @since 3.0
5470
*/
5571
public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
5672

5773
private static final boolean jsr303Present = ClassUtils.isPresent(
5874
"javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
5975

76+
private static final boolean jaxb2Present =
77+
ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
78+
79+
private static final boolean jacksonPresent =
80+
ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) &&
81+
ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
82+
83+
6084

6185
public BeanDefinition parse(Element element, ParserContext parserContext) {
6286
Object source = parserContext.extractSource(element);
@@ -74,6 +98,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
7498
RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class);
7599
annAdapterDef.setSource(source);
76100
annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
101+
annAdapterDef.getPropertyValues().add("messageConverters", getMessageConverters(source));
77102
String adapterName = parserContext.getReaderContext().registerWithGeneratedName(annAdapterDef);
78103

79104
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
@@ -113,4 +138,20 @@ else if (jsr303Present) {
113138
}
114139
}
115140

141+
private ManagedList<RootBeanDefinition> getMessageConverters(Object source) {
142+
ManagedList<RootBeanDefinition> messageConverters = new ManagedList<RootBeanDefinition>();
143+
messageConverters.setSource(source);
144+
messageConverters.add(new RootBeanDefinition(ByteArrayHttpMessageConverter.class));
145+
messageConverters.add(new RootBeanDefinition(StringHttpMessageConverter.class));
146+
messageConverters.add(new RootBeanDefinition(FormHttpMessageConverter.class));
147+
messageConverters.add(new RootBeanDefinition(SourceHttpMessageConverter.class));
148+
if (jaxb2Present) {
149+
messageConverters.add(new RootBeanDefinition(Jaxb2RootElementHttpMessageConverter.class));
150+
}
151+
if (jacksonPresent) {
152+
messageConverters.add(new RootBeanDefinition(MappingJacksonHttpMessageConverter.class));
153+
}
154+
return messageConverters;
155+
}
156+
116157
}

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,14 @@ public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAnd
319319
this.customModelAndViewResolvers = customModelAndViewResolvers;
320320
}
321321

322+
/**
323+
* Returns the message body converters to use. These converters are used to convert from and to HTTP requests and
324+
* responses.
325+
*/
326+
public HttpMessageConverter<?>[] getMessageConverters() {
327+
return messageConverters;
328+
}
329+
322330
/**
323331
* Set the message body converters to use. These converters are used to convert from and to HTTP requests and
324332
* responses.
@@ -605,7 +613,7 @@ private class ServletHandlerMethodInvoker extends HandlerMethodInvoker {
605613

606614
private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) {
607615
super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer,
608-
customArgumentResolvers, messageConverters);
616+
customArgumentResolvers, getMessageConverters());
609617
}
610618

611619
@Override
@@ -792,8 +800,8 @@ private void handleResponseBody(Object returnValue, ServletWebRequest webRequest
792800
HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
793801
Class<?> returnValueType = returnValue.getClass();
794802
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
795-
if (messageConverters != null) {
796-
for (HttpMessageConverter messageConverter : messageConverters) {
803+
if (getMessageConverters() != null) {
804+
for (HttpMessageConverter messageConverter : getMessageConverters()) {
797805
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
798806
for (MediaType acceptedMediaType : acceptedMediaTypes) {
799807
if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,15 @@
1616

1717
package org.springframework.web.servlet.config;
1818

19-
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.assertFalse;
21-
import static org.junit.Assert.assertNotNull;
22-
import static org.junit.Assert.assertNull;
23-
import static org.junit.Assert.assertTrue;
24-
2519
import java.util.Date;
2620
import java.util.Locale;
27-
2821
import javax.validation.Valid;
2922
import javax.validation.constraints.NotNull;
3023

24+
import static org.junit.Assert.*;
3125
import org.junit.Before;
3226
import org.junit.Test;
27+
3328
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
3429
import org.springframework.context.i18n.LocaleContextHolder;
3530
import org.springframework.core.convert.ConversionFailedException;
@@ -38,6 +33,13 @@
3833
import org.springframework.format.annotation.DateTimeFormat;
3934
import org.springframework.format.annotation.DateTimeFormat.ISO;
4035
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
36+
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
37+
import org.springframework.http.converter.FormHttpMessageConverter;
38+
import org.springframework.http.converter.HttpMessageConverter;
39+
import org.springframework.http.converter.StringHttpMessageConverter;
40+
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
41+
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
42+
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
4143
import org.springframework.mock.web.MockHttpServletRequest;
4244
import org.springframework.mock.web.MockHttpServletResponse;
4345
import org.springframework.mock.web.MockServletContext;
@@ -61,35 +63,47 @@
6163

6264
/**
6365
* @author Keith Donald
66+
* @author Arjen Poutsma
6467
*/
6568
public class MvcNamespaceTests {
6669

67-
private GenericWebApplicationContext container;
70+
private GenericWebApplicationContext appContext;
6871

6972
@Before
7073
public void setUp() {
71-
container = new GenericWebApplicationContext();
72-
container.setServletContext(new MockServletContext());
74+
appContext = new GenericWebApplicationContext();
75+
appContext.setServletContext(new MockServletContext());
7376
LocaleContextHolder.setLocale(Locale.US);
7477
}
7578

7679
@Test
7780
public void testDefaultConfig() throws Exception {
78-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
81+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
7982
reader.loadBeanDefinitions(new ClassPathResource("mvc-config.xml", getClass()));
80-
assertEquals(4, container.getBeanDefinitionCount());
81-
container.refresh();
83+
assertEquals(4, appContext.getBeanDefinitionCount());
84+
appContext.refresh();
8285

83-
DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class);
86+
DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class);
8487
assertNotNull(mapping);
8588
assertEquals(0, mapping.getOrder());
8689

87-
AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class);
90+
AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class);
8891
assertNotNull(adapter);
89-
assertNotNull(container.getBean(FormattingConversionServiceFactoryBean.class));
90-
assertNotNull(container.getBean(ConversionService.class));
91-
assertNotNull(container.getBean(LocalValidatorFactoryBean.class));
92-
assertNotNull(container.getBean(Validator.class));
92+
93+
HttpMessageConverter[] messageConverters = adapter.getMessageConverters();
94+
assertNotNull(messageConverters);
95+
assertEquals(6, messageConverters.length);
96+
assertTrue(ByteArrayHttpMessageConverter.class.equals(messageConverters[0].getClass()));
97+
assertTrue(StringHttpMessageConverter.class.equals(messageConverters[1].getClass()));
98+
assertTrue(FormHttpMessageConverter.class.equals(messageConverters[2].getClass()));
99+
assertTrue(SourceHttpMessageConverter.class.equals(messageConverters[3].getClass()));
100+
assertTrue(Jaxb2RootElementHttpMessageConverter.class.equals(messageConverters[4].getClass()));
101+
assertTrue(MappingJacksonHttpMessageConverter.class.equals(messageConverters[5].getClass()));
102+
103+
assertNotNull(appContext.getBean(FormattingConversionServiceFactoryBean.class));
104+
assertNotNull(appContext.getBean(ConversionService.class));
105+
assertNotNull(appContext.getBean(LocalValidatorFactoryBean.class));
106+
assertNotNull(appContext.getBean(Validator.class));
93107

94108
TestController handler = new TestController();
95109

@@ -103,12 +117,12 @@ public void testDefaultConfig() throws Exception {
103117

104118
@Test(expected=ConversionFailedException.class)
105119
public void testCustomConversionService() throws Exception {
106-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
120+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
107121
reader.loadBeanDefinitions(new ClassPathResource("mvc-config-custom-conversion-service.xml", getClass()));
108-
assertEquals(4, container.getBeanDefinitionCount());
109-
container.refresh();
122+
assertEquals(4, appContext.getBeanDefinitionCount());
123+
appContext.refresh();
110124

111-
AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class);
125+
AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class);
112126
assertNotNull(adapter);
113127

114128
TestController handler = new TestController();
@@ -122,12 +136,12 @@ public void testCustomConversionService() throws Exception {
122136

123137
@Test
124138
public void testCustomValidator() throws Exception {
125-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
139+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
126140
reader.loadBeanDefinitions(new ClassPathResource("mvc-config-custom-validator.xml", getClass()));
127-
assertEquals(4, container.getBeanDefinitionCount());
128-
container.refresh();
141+
assertEquals(4, appContext.getBeanDefinitionCount());
142+
appContext.refresh();
129143

130-
AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class);
144+
AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class);
131145
assertNotNull(adapter);
132146

133147
TestController handler = new TestController();
@@ -138,18 +152,18 @@ public void testCustomValidator() throws Exception {
138152
MockHttpServletResponse response = new MockHttpServletResponse();
139153
adapter.handle(request, response, handler);
140154

141-
assertTrue(container.getBean(TestValidator.class).validatorInvoked);
155+
assertTrue(appContext.getBean(TestValidator.class).validatorInvoked);
142156
assertFalse(handler.recordedValidationError);
143157
}
144158

145159
@Test
146160
public void testInterceptors() throws Exception {
147-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
161+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
148162
reader.loadBeanDefinitions(new ClassPathResource("mvc-config-interceptors.xml", getClass()));
149-
assertEquals(7, container.getBeanDefinitionCount());
150-
container.refresh();
163+
assertEquals(7, appContext.getBeanDefinitionCount());
164+
appContext.refresh();
151165

152-
DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class);
166+
DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class);
153167
assertNotNull(mapping);
154168
mapping.setDefaultHandler(new TestController());
155169

@@ -177,12 +191,12 @@ public void testInterceptors() throws Exception {
177191

178192
@Test
179193
public void testBeanDecoration() throws Exception {
180-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
194+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
181195
reader.loadBeanDefinitions(new ClassPathResource("mvc-config-bean-decoration.xml", getClass()));
182-
assertEquals(6, container.getBeanDefinitionCount());
183-
container.refresh();
196+
assertEquals(6, appContext.getBeanDefinitionCount());
197+
appContext.refresh();
184198

185-
DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class);
199+
DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class);
186200
assertNotNull(mapping);
187201
mapping.setDefaultHandler(new TestController());
188202

@@ -199,12 +213,12 @@ public void testBeanDecoration() throws Exception {
199213

200214
@Test
201215
public void testViewControllers() throws Exception {
202-
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container);
216+
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext);
203217
reader.loadBeanDefinitions(new ClassPathResource("mvc-config-view-controllers.xml", getClass()));
204-
assertEquals(8, container.getBeanDefinitionCount());
205-
container.refresh();
218+
assertEquals(8, appContext.getBeanDefinitionCount());
219+
appContext.refresh();
206220

207-
DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class);
221+
DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class);
208222
assertNotNull(mapping);
209223
mapping.setDefaultHandler(new TestController());
210224

@@ -214,10 +228,10 @@ public void testViewControllers() throws Exception {
214228
assertEquals(3, chain.getInterceptors().length);
215229
assertTrue(chain.getInterceptors()[1] instanceof LocaleChangeInterceptor);
216230

217-
SimpleUrlHandlerMapping mapping2 = container.getBean(SimpleUrlHandlerMapping.class);
231+
SimpleUrlHandlerMapping mapping2 = appContext.getBean(SimpleUrlHandlerMapping.class);
218232
assertNotNull(mapping2);
219233

220-
SimpleControllerHandlerAdapter adapter = container.getBean(SimpleControllerHandlerAdapter.class);
234+
SimpleControllerHandlerAdapter adapter = appContext.getBean(SimpleControllerHandlerAdapter.class);
221235
assertNotNull(adapter);
222236

223237
request.setRequestURI("/foo");

0 commit comments

Comments
 (0)