From a22ab375cc3ecd3621ba9704b74bcbc8e372be79 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 30 Nov 2009 12:21:13 +0000 Subject: [PATCH] SPR-6375 - Register sensible default HTTP Message Converters based on what is available in the classpath git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@2547 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../AnnotationDrivenBeanDefinitionParser.java | 59 +++++++++-- .../AnnotationMethodHandlerAdapter.java | 14 ++- .../web/servlet/config/MvcNamespaceTests.java | 98 +++++++++++-------- 3 files changed, 117 insertions(+), 54 deletions(-) diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 86912f52888..8a3dc70120c 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -16,40 +16,56 @@ package org.springframework.web.servlet.config; +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.ClassUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; -import org.w3c.dom.Element; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses the {@code annotation-driven} element to configure - * a Spring MVC web application. - *

- * Responsible for: + * {@link BeanDefinitionParser} that parses the {@code annotation-driven} element to configure a Spring MVC web + * application. + * + *

Responsible for: *

    - *
  1. Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods using @RequestMapping annotations. + *
  2. Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods + * using @RequestMapping annotations. *
  3. Registering a AnnotationMethodHandlerAdapter bean for invoking annotated @Controller methods. - * Will configure the HandlerAdapter's webBindingInitializer property for centrally configuring @Controller DataBinder instances: + * Will configure the HandlerAdapter's webBindingInitializer property for centrally configuring + * {@code @Controller} {@code DataBinder} instances: *
      - *
    • Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance created by the default {@link FormattingConversionServiceFactoryBean}. - *
    • Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the default {@link LocalValidatorFactoryBean} if the JSR-303 API is present in the classpath. + *
    • Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance + * created by the default {@link FormattingConversionServiceFactoryBean}. + *
    • Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the + * default {@link LocalValidatorFactoryBean} if the JSR-303 API is present on the classpath. + *
    • Configures standard {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}, + * including the {@link Jaxb2RootElementHttpMessageConverter} if JAXB2 is present on the classpath, and + * the {@link MappingJacksonHttpMessageConverter} if Jackson is present on the classpath. *
    *
* * @author Keith Donald * @author Juergen Hoeller + * @author Arjen Poutsma * @since 3.0 */ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -57,6 +73,14 @@ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParse private static final boolean jsr303Present = ClassUtils.isPresent( "javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + private static final boolean jaxb2Present = + ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jacksonPresent = + ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) && + ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + public BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); @@ -74,6 +98,7 @@ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParse RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); annAdapterDef.setSource(source); annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); + annAdapterDef.getPropertyValues().add("messageConverters", getMessageConverters(source)); String adapterName = parserContext.getReaderContext().registerWithGeneratedName(annAdapterDef); CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); @@ -113,4 +138,20 @@ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParse } } + private ManagedList getMessageConverters(Object source) { + ManagedList messageConverters = new ManagedList(); + messageConverters.setSource(source); + messageConverters.add(new RootBeanDefinition(ByteArrayHttpMessageConverter.class)); + messageConverters.add(new RootBeanDefinition(StringHttpMessageConverter.class)); + messageConverters.add(new RootBeanDefinition(FormHttpMessageConverter.class)); + messageConverters.add(new RootBeanDefinition(SourceHttpMessageConverter.class)); + if (jaxb2Present) { + messageConverters.add(new RootBeanDefinition(Jaxb2RootElementHttpMessageConverter.class)); + } + if (jacksonPresent) { + messageConverters.add(new RootBeanDefinition(MappingJacksonHttpMessageConverter.class)); + } + return messageConverters; + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index e7ba282e2a5..6f33665f96e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -319,6 +319,14 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen this.customModelAndViewResolvers = customModelAndViewResolvers; } + /** + * Returns the message body converters to use. These converters are used to convert from and to HTTP requests and + * responses. + */ + public HttpMessageConverter[] getMessageConverters() { + return messageConverters; + } + /** * Set the message body converters to use. These converters are used to convert from and to HTTP requests and * responses. @@ -605,7 +613,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) { super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer, - customArgumentResolvers, messageConverters); + customArgumentResolvers, getMessageConverters()); } @Override @@ -792,8 +800,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); Class returnValueType = returnValue.getClass(); List allSupportedMediaTypes = new ArrayList(); - if (messageConverters != null) { - for (HttpMessageConverter messageConverter : messageConverters) { + if (getMessageConverters() != null) { + for (HttpMessageConverter messageConverter : getMessageConverters()) { allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); for (MediaType acceptedMediaType : acceptedMediaTypes) { if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index b9f8eeea1d1..459f066cf5a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -16,20 +16,15 @@ package org.springframework.web.servlet.config; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - import java.util.Date; import java.util.Locale; - import javax.validation.Valid; import javax.validation.constraints.NotNull; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.convert.ConversionFailedException; @@ -38,6 +33,13 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; @@ -61,35 +63,47 @@ import org.springframework.web.servlet.theme.ThemeChangeInterceptor; /** * @author Keith Donald + * @author Arjen Poutsma */ public class MvcNamespaceTests { - private GenericWebApplicationContext container; + private GenericWebApplicationContext appContext; @Before public void setUp() { - container = new GenericWebApplicationContext(); - container.setServletContext(new MockServletContext()); + appContext = new GenericWebApplicationContext(); + appContext.setServletContext(new MockServletContext()); LocaleContextHolder.setLocale(Locale.US); } @Test public void testDefaultConfig() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config.xml", getClass())); - assertEquals(4, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(4, appContext.getBeanDefinitionCount()); + appContext.refresh(); - DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class); + DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class); assertNotNull(mapping); assertEquals(0, mapping.getOrder()); - AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class); + AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class); assertNotNull(adapter); - assertNotNull(container.getBean(FormattingConversionServiceFactoryBean.class)); - assertNotNull(container.getBean(ConversionService.class)); - assertNotNull(container.getBean(LocalValidatorFactoryBean.class)); - assertNotNull(container.getBean(Validator.class)); + + HttpMessageConverter[] messageConverters = adapter.getMessageConverters(); + assertNotNull(messageConverters); + assertEquals(6, messageConverters.length); + assertTrue(ByteArrayHttpMessageConverter.class.equals(messageConverters[0].getClass())); + assertTrue(StringHttpMessageConverter.class.equals(messageConverters[1].getClass())); + assertTrue(FormHttpMessageConverter.class.equals(messageConverters[2].getClass())); + assertTrue(SourceHttpMessageConverter.class.equals(messageConverters[3].getClass())); + assertTrue(Jaxb2RootElementHttpMessageConverter.class.equals(messageConverters[4].getClass())); + assertTrue(MappingJacksonHttpMessageConverter.class.equals(messageConverters[5].getClass())); + + assertNotNull(appContext.getBean(FormattingConversionServiceFactoryBean.class)); + assertNotNull(appContext.getBean(ConversionService.class)); + assertNotNull(appContext.getBean(LocalValidatorFactoryBean.class)); + assertNotNull(appContext.getBean(Validator.class)); TestController handler = new TestController(); @@ -103,12 +117,12 @@ public class MvcNamespaceTests { @Test(expected=ConversionFailedException.class) public void testCustomConversionService() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config-custom-conversion-service.xml", getClass())); - assertEquals(4, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(4, appContext.getBeanDefinitionCount()); + appContext.refresh(); - AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class); + AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class); assertNotNull(adapter); TestController handler = new TestController(); @@ -122,12 +136,12 @@ public class MvcNamespaceTests { @Test public void testCustomValidator() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config-custom-validator.xml", getClass())); - assertEquals(4, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(4, appContext.getBeanDefinitionCount()); + appContext.refresh(); - AnnotationMethodHandlerAdapter adapter = container.getBean(AnnotationMethodHandlerAdapter.class); + AnnotationMethodHandlerAdapter adapter = appContext.getBean(AnnotationMethodHandlerAdapter.class); assertNotNull(adapter); TestController handler = new TestController(); @@ -138,18 +152,18 @@ public class MvcNamespaceTests { MockHttpServletResponse response = new MockHttpServletResponse(); adapter.handle(request, response, handler); - assertTrue(container.getBean(TestValidator.class).validatorInvoked); + assertTrue(appContext.getBean(TestValidator.class).validatorInvoked); assertFalse(handler.recordedValidationError); } @Test public void testInterceptors() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config-interceptors.xml", getClass())); - assertEquals(7, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(7, appContext.getBeanDefinitionCount()); + appContext.refresh(); - DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class); + DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class); assertNotNull(mapping); mapping.setDefaultHandler(new TestController()); @@ -177,12 +191,12 @@ public class MvcNamespaceTests { @Test public void testBeanDecoration() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config-bean-decoration.xml", getClass())); - assertEquals(6, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(6, appContext.getBeanDefinitionCount()); + appContext.refresh(); - DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class); + DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class); assertNotNull(mapping); mapping.setDefaultHandler(new TestController()); @@ -199,12 +213,12 @@ public class MvcNamespaceTests { @Test public void testViewControllers() throws Exception { - XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(container); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); reader.loadBeanDefinitions(new ClassPathResource("mvc-config-view-controllers.xml", getClass())); - assertEquals(8, container.getBeanDefinitionCount()); - container.refresh(); + assertEquals(8, appContext.getBeanDefinitionCount()); + appContext.refresh(); - DefaultAnnotationHandlerMapping mapping = container.getBean(DefaultAnnotationHandlerMapping.class); + DefaultAnnotationHandlerMapping mapping = appContext.getBean(DefaultAnnotationHandlerMapping.class); assertNotNull(mapping); mapping.setDefaultHandler(new TestController()); @@ -214,10 +228,10 @@ public class MvcNamespaceTests { assertEquals(3, chain.getInterceptors().length); assertTrue(chain.getInterceptors()[1] instanceof LocaleChangeInterceptor); - SimpleUrlHandlerMapping mapping2 = container.getBean(SimpleUrlHandlerMapping.class); + SimpleUrlHandlerMapping mapping2 = appContext.getBean(SimpleUrlHandlerMapping.class); assertNotNull(mapping2); - SimpleControllerHandlerAdapter adapter = container.getBean(SimpleControllerHandlerAdapter.class); + SimpleControllerHandlerAdapter adapter = appContext.getBean(SimpleControllerHandlerAdapter.class); assertNotNull(adapter); request.setRequestURI("/foo");