From 82a26024ae17b7d3c8ba5f805c853a6df0a072e1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 7 Aug 2013 18:43:13 -0400 Subject: [PATCH] Allow @ResponseBody on the type level This change enables having @ResponseBody on the type-level in which case it inherited and does not need to be added on the method level. For added convenience, there is also a new @RestController annotation, a meta-annotation in turn annotated with @Controller and @ResponseBody. Classes with the new annotation do not need to have @ResponseBody declared on the method level as it is inherited. Issue: SPR-10814 --- .../web/bind/annotation/ResponseBody.java | 13 +++-- .../web/bind/annotation/RestController.java | 45 ++++++++++++++++ .../RequestResponseBodyMethodProcessor.java | 3 +- ...questResponseBodyMethodProcessorTests.java | 54 +++++++++++++++++-- ...nnotationControllerHandlerMethodTests.java | 41 +++++++++----- 5 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java index 69ec801103..7a8349ec04 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-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. @@ -23,15 +23,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Annotation which indicates that a method return value should be bound to the web response body. - * Supported for annotated handler methods in Servlet environments. + * Annotation that indicates a method return value should be bound to the web response + * body. Supported for annotated handler methods in Servlet environments. + *

+ * As of version 4.0 this annotation can also be added on the type level in which case + * is inherited and does not need to be added on the method level. * * @author Arjen Poutsma * @since 3.0 * @see RequestBody - * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @see RestController */ -@Target(ElementType.METHOD) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseBody { diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java new file mode 100644 index 0000000000..aa93d35e2d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; + +/** + * A convenience annotation that is itself annotated with {@link Controller @Controller} + * and {@link ResponseBody @ResponseBody}. + *

+ * Types that carry this annotation are treated as + * controllers where {@link RequestMapping @RequestMapping} methods assume + * {@link ResponseBody @ResponseBody} semantics by default. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +@ResponseBody +public @interface RestController { + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 0e2cc60956..ba32ccbb9c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -81,7 +81,8 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter @Override public boolean supportsReturnType(MethodParameter returnType) { - return returnType.getMethodAnnotation(ResponseBody.class) != null; + return ((AnnotationUtils.findAnnotation(returnType.getDeclaringClass(), ResponseBody.class) != null) + || (returnType.getMethodAnnotation(ResponseBody.class) != null)); } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 5c69b6f424..56e2533d46 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -16,9 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; @@ -39,12 +36,17 @@ import org.springframework.util.MultiValueMap; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; +import static org.junit.Assert.*; + /** * Test fixture for a {@link RequestResponseBodyMethodProcessor} with actual delegation * to HttpMessageConverter instances. @@ -231,6 +233,34 @@ public class RequestResponseBodyMethodProcessorTests { assertEquals("text/plain;charset=UTF-8", servletResponse.getHeader("Content-Type")); } + @Test + public void supportsReturnTypeResponseBodyOnType() throws Exception { + + Method method = ResponseBodyController.class.getMethod("handle"); + MethodParameter returnType = new MethodParameter(method, -1); + + List> converters = new ArrayList>(); + converters.add(new StringHttpMessageConverter()); + + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + assertTrue("Failed to recognize type-level @ResponseBody", processor.supportsReturnType(returnType)); + } + + @Test + public void supportsReturnTypeRestController() throws Exception { + + Method method = TestRestController.class.getMethod("handle"); + MethodParameter returnType = new MethodParameter(method, -1); + + List> converters = new ArrayList>(); + converters.add(new StringHttpMessageConverter()); + + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + assertTrue("Failed to recognize type-level @RestController", processor.supportsReturnType(returnType)); + } + public String handle( @RequestBody List list, @@ -289,4 +319,22 @@ public class RequestResponseBodyMethodProcessorTests { } } + @ResponseBody + private static class ResponseBodyController { + + @RequestMapping + public String handle() { + return "hello"; + } + } + + @RestController + private static class TestRestController { + + @RequestMapping + public String handle() { + return "hello"; + } + } + } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 6c5c7327e0..d9109a2738 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -16,14 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -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.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.beans.PropertyEditorSupport; import java.io.IOException; import java.io.Serializable; @@ -64,10 +56,6 @@ import org.junit.Test; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; -import org.springframework.tests.sample.beans.DerivedTestBean; -import org.springframework.tests.sample.beans.GenericBean; -import org.springframework.tests.sample.beans.ITestBean; -import org.springframework.tests.sample.beans.TestBean; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -101,6 +89,10 @@ import org.springframework.mock.web.test.MockServletConfig; import org.springframework.mock.web.test.MockServletContext; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.stereotype.Controller; +import org.springframework.tests.sample.beans.DerivedTestBean; +import org.springframework.tests.sample.beans.GenericBean; +import org.springframework.tests.sample.beans.ITestBean; +import org.springframework.tests.sample.beans.TestBean; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; @@ -123,6 +115,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebArgumentResolver; @@ -142,6 +135,8 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.view.InternalResourceViewResolver; +import static org.junit.Assert.*; + /** * The origin of this test class is {@link ServletAnnotationControllerHandlerMethodTests}. * @@ -1569,6 +1564,18 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals("count:3", response.getContentAsString()); } + @Test + public void restController() throws Exception { + + initServletWithControllers(ThisWillActuallyRun.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("Hello World!", response.getContentAsString()); + } + + /* * Controllers */ @@ -2979,6 +2986,16 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } } + @RestController + static class ThisWillActuallyRun { + + @RequestMapping(value = "/", method = RequestMethod.GET) + public String home() { + return "Hello World!"; + } + } + + // Test cases deleted from the original SevletAnnotationControllerTests: // @Ignore("Controller interface => no method-level @RequestMapping annotation")