Support attribute overrides with @ResponseStatus

This commit introduces support for attribute overrides for
@ResponseStatus when @ResponseStatus is used as a meta-annotation on
a custom composed annotation.

Specifically, this commit migrates all code that looks up
@ResponseStatus from using AnnotationUtils.findAnnotation() to using
AnnotatedElementUtils.findMergedAnnotation().

Issue: SPR-13441
This commit is contained in:
Sam Brannen 2015-09-11 20:31:44 +02:00
parent 4a49ce9694
commit e2bfbdcfd1
8 changed files with 164 additions and 65 deletions

View File

@ -16,8 +16,12 @@
package org.springframework.test.web.servlet.samples.standalone.resultmatchers;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.servlet.MockMvc;
@ -26,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import static org.hamcrest.Matchers.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
@ -34,6 +39,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
* Examples of expectations on the status and the status reason found in the response.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
*/
public class StatusAssertionTests {
@ -42,12 +48,14 @@ public class StatusAssertionTests {
@Test
public void testStatusInt() throws Exception {
this.mockMvc.perform(get("/created")).andExpect(status().is(201));
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().is(201));
this.mockMvc.perform(get("/badRequest")).andExpect(status().is(400));
}
@Test
public void testHttpStatus() throws Exception {
this.mockMvc.perform(get("/created")).andExpect(status().isCreated());
this.mockMvc.perform(get("/createdWithComposedAnnotation")).andExpect(status().isCreated());
this.mockMvc.perform(get("/badRequest")).andExpect(status().isBadRequest());
}
@ -66,27 +74,43 @@ public class StatusAssertionTests {
@Test
public void testReasonMatcher() throws Exception {
this.mockMvc.perform(get("/badRequest"))
.andExpect(status().reason(endsWith("token")));
this.mockMvc.perform(get("/badRequest")).andExpect(status().reason(endsWith("token")));
}
@RequestMapping
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface Get {
@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] path() default {};
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus status() default INTERNAL_SERVER_ERROR;
}
@Controller
private static class StatusController {
@RequestMapping("/created")
@ResponseStatus(HttpStatus.CREATED)
@ResponseStatus(CREATED)
public @ResponseBody void created(){
}
@Get(path = "/createdWithComposedAnnotation", status = CREATED)
public @ResponseBody void createdWithComposedAnnotation() {
}
@RequestMapping("/badRequest")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Expired token")
@ResponseStatus(code = BAD_REQUEST, reason = "Expired token")
public @ResponseBody void badRequest(){
}
@RequestMapping("/notImplemented")
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
@ResponseStatus(NOT_IMPLEMENTED)
public @ResponseBody void notImplemented(){
}
}
}

View File

@ -25,7 +25,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -33,15 +33,17 @@ import org.springframework.util.ClassUtils;
/**
* Encapsulates information about a handler method consisting of a
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
* Provides convenient access to method parameters, method return value, method annotations.
* Provides convenient access to method parameters, the method return value,
* method annotations, etc.
*
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean,
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@link HandlerMethod}
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@code HandlerMethod}
* instance with a bean instance resolved through the associated {@link BeanFactory}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.1
*/
public class HandlerMethod {
@ -98,7 +100,7 @@ public class HandlerMethod {
/**
* Create an instance from a bean name, a method, and a {@code BeanFactory}.
* The method {@link #createWithResolvedBean()} may be used later to
* re-create the {@code HandlerMethod} with an initialized the bean.
* re-create the {@code HandlerMethod} with an initialized bean.
*/
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
Assert.hasText(beanName, "Bean name is required");
@ -222,11 +224,14 @@ public class HandlerMethod {
/**
* Returns a single annotation on the underlying method traversing its super methods
* if no annotation can be found on the given method itself.
* <p>Also supports <em>merged</em> composed annotations with attribute
* overrides as of Spring Framework 4.2.2.
* @param annotationType the type of annotation to introspect the method for.
* @return the annotation, or {@code null} if none found
* @see AnnotatedElementUtils#findMergedAnnotation
*/
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
return AnnotationUtils.findAnnotation(this.method, annotationType);
return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
}
/**

View File

@ -56,6 +56,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@ -119,7 +120,7 @@ import org.springframework.web.util.WebUtils;
/**
* Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface
* that maps handler methods based on HTTP paths, HTTP methods and request parameters
* that maps handler methods based on HTTP paths, HTTP methods, and request parameters
* expressed through the {@link RequestMapping} annotation.
*
* <p>Supports request parameter binding through the {@link RequestParam} annotation.
@ -133,6 +134,7 @@ import org.springframework.web.util.WebUtils;
*
* @author Juergen Hoeller
* @author Arjen Poutsma
* @author Sam Brannen
* @since 2.5
* @see #setPathMatcher
* @see #setMethodNameResolver
@ -911,19 +913,19 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator
public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue,
ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception {
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatusAnn != null) {
HttpStatus responseStatus = responseStatusAnn.code();
String reason = responseStatusAnn.reason();
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatus != null) {
HttpStatus statusCode = responseStatus.code();
String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) {
webRequest.getResponse().setStatus(responseStatus.value());
webRequest.getResponse().setStatus(statusCode.value());
}
else {
webRequest.getResponse().sendError(responseStatus.value(), reason);
webRequest.getResponse().sendError(statusCode.value(), reason);
}
// to be picked up by the RedirectView
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus);
webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode);
this.responseArgumentUsed = true;
}

View File

@ -43,6 +43,7 @@ import javax.xml.transform.Source;
import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpInputMessage;
@ -377,15 +378,15 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc
private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
throws Exception {
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatusAnn != null) {
HttpStatus responseStatus = responseStatusAnn.code();
String reason = responseStatusAnn.reason();
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
if (responseStatus != null) {
HttpStatus statusCode = responseStatus.code();
String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) {
webRequest.getResponse().setStatus(responseStatus.value());
webRequest.getResponse().setStatus(statusCode.value());
}
else {
webRequest.getResponse().sendError(responseStatus.value(), reason);
webRequest.getResponse().sendError(statusCode.value(), reason);
}
}

View File

@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
@ -38,11 +38,14 @@ import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
* and the MVC Java config and the MVC namespace.
*
* <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus}
* present on cause exceptions.
* present on cause exceptions, and as of 4.2.2 this resolver supports
* attribute overrides for {@code @ResponseStatus} in custom composed annotations.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 3.0
* @see AnnotatedElementUtils#findMergedAnnotation
*/
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
@ -59,7 +62,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
try {
return resolveResponseStatus(responseStatus, request, response, handler, ex);

View File

@ -20,19 +20,20 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.BindException;
import java.net.SocketException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ -43,25 +44,18 @@ import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Sam Brannen
*/
@Deprecated
public class AnnotationMethodHandlerExceptionResolverTests {
private AnnotationMethodHandlerExceptionResolver exceptionResolver;
private final AnnotationMethodHandlerExceptionResolver exceptionResolver = new AnnotationMethodHandlerExceptionResolver();
private MockHttpServletRequest request;
private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
private MockHttpServletResponse response;
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Before
public void setUp() {
exceptionResolver = new AnnotationMethodHandlerExceptionResolver();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");
}
@Test
public void simpleWithIOException() {
IOException ex = new IOException();
@ -103,6 +97,16 @@ public class AnnotationMethodHandlerExceptionResolverTests {
assertEquals("Invalid status code returned", 406, response.getStatus());
}
@Test
public void simpleWithNumberFormatExceptionAndComposedResponseStatusAnnotation() {
NumberFormatException ex = new NumberFormatException();
SimpleController controller = new SimpleController();
ModelAndView mav = exceptionResolver.resolveException(request, response, controller, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "X:NumberFormatException", mav.getViewName());
assertEquals("Invalid status code returned", 400, response.getStatus());
}
@Test
public void inherited() {
IOException ex = new IOException();
@ -155,6 +159,13 @@ public class AnnotationMethodHandlerExceptionResolverTests {
assertNull(mav);
}
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@Controller
private static class SimpleController {
@ -162,18 +173,24 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleIOException(IOException ex, HttpServletRequest request) {
return "X:" + ClassUtils.getShortName(ex.getClass());
return "X:" + ex.getClass().getSimpleName();
}
@ExceptionHandler(SocketException.class)
@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!")
public String handleSocketException(Exception ex, HttpServletResponse response) {
return "Y:" + ClassUtils.getShortName(ex.getClass());
return "Y:" + ex.getClass().getSimpleName();
}
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(Exception ex) {
return ClassUtils.getShortName(ex.getClass());
return ex.getClass().getSimpleName();
}
@ExceptionHandler(NumberFormatException.class)
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
public String handleNumberFormatException(NumberFormatException ex) {
return "X:" + ex.getClass().getSimpleName();
}
}
@ -194,12 +211,12 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler({BindException.class, IllegalArgumentException.class})
public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response)
throws IOException {
return ClassUtils.getShortName(ex.getClass());
return ex.getClass().getSimpleName();
}
@ExceptionHandler
public String handle2(IllegalArgumentException ex) {
return ClassUtils.getShortName(ex.getClass());
return ex.getClass().getSimpleName();
}
}
@ -209,7 +226,7 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(Exception.class)
public void handle(Exception ex, Writer writer) throws IOException {
writer.write(ClassUtils.getShortName(ex.getClass()));
writer.write(ex.getClass().getSimpleName());
}
}
@ -220,7 +237,7 @@ public class AnnotationMethodHandlerExceptionResolverTests {
@ExceptionHandler(Exception.class)
@ResponseBody
public String handle(Exception ex) {
return ClassUtils.getShortName(ex.getClass());
return ex.getClass().getSimpleName();
}
}

View File

@ -16,16 +16,16 @@
package org.springframework.web.servlet.mvc.annotation;
import static org.junit.Assert.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
@ -33,22 +33,21 @@ import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
/** @author Arjen Poutsma */
import static org.junit.Assert.*;
/**
* Integration tests for {@link ResponseStatusExceptionResolver}.
*
* @author Arjen Poutsma
* @author Sam Brannen
*/
public class ResponseStatusExceptionResolverTests {
private ResponseStatusExceptionResolver exceptionResolver;
private final ResponseStatusExceptionResolver exceptionResolver = new ResponseStatusExceptionResolver();
private MockHttpServletRequest request;
private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
private MockHttpServletResponse response;
@Before
public void setUp() {
exceptionResolver = new ResponseStatusExceptionResolver();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");
}
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
public void statusCode() {
@ -60,6 +59,16 @@ public class ResponseStatusExceptionResolverTests {
assertTrue("Response has not been committed", response.isCommitted());
}
@Test
public void statusCodeFromComposedResponseStatus() {
StatusCodeFromComposedResponseStatusException ex = new StatusCodeFromComposedResponseStatusException();
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertNotNull("No ModelAndView returned", mav);
assertTrue("No Empty ModelAndView returned", mav.isEmpty());
assertEquals("Invalid status code", 400, response.getStatus());
assertTrue("Response has not been committed", response.isCommitted());
}
@Test
public void statusCodeAndReason() {
StatusCodeAndReasonException ex = new StatusCodeAndReasonException();
@ -109,6 +118,7 @@ public class ResponseStatusExceptionResolverTests {
assertEquals("Invalid status code", 410, response.getStatus());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@SuppressWarnings("serial")
private static class StatusCodeException extends Exception {
@ -124,4 +134,17 @@ public class ResponseStatusExceptionResolverTests {
private static class StatusCodeAndReasonMessageException extends Exception {
}
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
@SuppressWarnings("serial")
private static class StatusCodeFromComposedResponseStatusException extends Exception {
}
}

View File

@ -16,9 +16,8 @@
package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
@ -29,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
@ -49,6 +49,9 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandlerCom
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.view.RedirectView;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Test fixture with {@link ServletInvocableHandlerMethod}.
*
@ -80,6 +83,16 @@ public class ServletInvocableHandlerMethodTests {
assertEquals(HttpStatus.BAD_REQUEST.value(), this.response.getStatus());
}
@Test
public void invokeAndHandle_VoidWithComposedResponseStatus() throws Exception {
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "composedResponseStatus");
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
assertTrue("Null return value + @ComposedResponseStatus should result in 'request handled'",
this.mavContainer.isRequestHandled());
assertEquals(HttpStatus.BAD_REQUEST.value(), this.response.getStatus());
}
@Test
public void invokeAndHandle_VoidWithHttpServletResponseArgument() throws Exception {
this.argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver());
@ -260,6 +273,13 @@ public class ServletInvocableHandlerMethodTests {
return handlerMethod;
}
@ResponseStatus
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedResponseStatus {
@AliasFor(annotation = ResponseStatus.class, attribute = "code")
HttpStatus responseStatus() default HttpStatus.INTERNAL_SERVER_ERROR;
}
@SuppressWarnings("unused")
private static class Handler {
@ -277,6 +297,10 @@ public class ServletInvocableHandlerMethodTests {
return "foo";
}
@ComposedResponseStatus(responseStatus = HttpStatus.BAD_REQUEST)
public void composedResponseStatus() {
}
public void httpServletResponse(HttpServletResponse response) {
}