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

@@ -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) {
}