diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java index b9b9f788e0..c62ea4d4e8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -16,6 +16,7 @@ package org.springframework.web.servlet.mvc.annotation; +import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,6 +26,7 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; @@ -41,6 +43,8 @@ import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; * present on cause exceptions, and as of 4.2.2 this resolver supports * attribute overrides for {@code @ResponseStatus} in custom composed annotations. * + *

As of 5.0 this resolver also supports {@link ResponseStatusException}. + * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sam Brannen @@ -62,43 +66,80 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class); - if (responseStatus != null) { - try { - return resolveResponseStatus(responseStatus, request, response, handler, ex); + try { + if (ex instanceof ResponseStatusException) { + return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler); + } + + ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class); + if (status != null) { + return resolveResponseStatus(status, request, response, handler, ex); } - catch (Exception resolveEx) { - logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx); + + if (ex.getCause() instanceof Exception) { + ex = (Exception) ex.getCause(); + return doResolveException(request, response, handler, ex); } } - else if (ex.getCause() instanceof Exception) { - ex = (Exception) ex.getCause(); - return doResolveException(request, response, handler, ex); + catch (Exception resolveEx) { + logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx); } return null; } /** - * Template method that handles {@link ResponseStatus @ResponseStatus} annotation. - *

The default implementation sends a response error using - * {@link HttpServletResponse#sendError(int)} or - * {@link HttpServletResponse#sendError(int, String)} if the annotation has a - * {@linkplain ResponseStatus#reason() reason} and then returns an empty ModelAndView. - * @param responseStatus the annotation + * Template method that handles the {@link ResponseStatus @ResponseStatus} annotation. + *

The default implementation delegates to {@link #applyStatusAndReason} + * with the status code and reason from the annotation. + * @param responseStatus the {@code @ResponseStatus} annotation * @param request current HTTP request * @param response current HTTP response * @param handler the executed handler, or {@code null} if none chosen at the - * time of the exception (for example, if multipart resolution failed) - * @param ex the exception that got thrown during handler execution or the - * exception that has the ResponseStatus annotation if found on the cause. - * @return a corresponding ModelAndView to forward to, or {@code null} - * for default processing + * time of the exception, e.g. if multipart resolution failed + * @param ex the exception + * @return an empty ModelAndView, i.e. exception resolved */ - protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, - HttpServletResponse response, Object handler, Exception ex) throws Exception { + protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { int statusCode = responseStatus.code().value(); String reason = responseStatus.reason(); + return applyStatusAndReason(statusCode, reason, response); + } + + /** + * Template method that handles an {@link ResponseStatusException}. + *

The default implementation delegates to {@link #applyStatusAndReason} + * with the status code and reason from the exception. + * @param ex the exception + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen at the + * time of the exception, e.g. if multipart resolution failed + * @return an empty ModelAndView, i.e. exception resolved + * @since 5.0 + */ + protected ModelAndView resolveResponseStatusException(ResponseStatusException ex, + HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + int statusCode = ex.getStatus().value(); + String reason = ex.getReason(); + applyStatusAndReason(statusCode, reason, response); + return new ModelAndView(); + } + + /** + * Apply the resolved status code and reason to the response. + *

The default implementation sends a response error using + * {@link HttpServletResponse#sendError(int)} or + * {@link HttpServletResponse#sendError(int, String)} if there is a reason + * and then returns an empty ModelAndView. + * @since 5.0 + */ + protected ModelAndView applyStatusAndReason(int statusCode, String reason, HttpServletResponse response) + throws IOException { + if (this.messageSource != null) { reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java index f5bc12712a..53fed883d0 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolverTests.java @@ -32,6 +32,7 @@ import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.tests.sample.beans.ITestBean; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.ModelAndView; import static org.junit.Assert.*; @@ -41,6 +42,7 @@ import static org.junit.Assert.*; * * @author Arjen Poutsma * @author Sam Brannen + * @author Rossen Stoyanchev */ public class ResponseStatusExceptionResolverTests { @@ -50,40 +52,32 @@ public class ResponseStatusExceptionResolverTests { private final MockHttpServletResponse response = new MockHttpServletResponse(); + @Before public void setup() { exceptionResolver.setWarnLogCategory(exceptionResolver.getClass().getName()); } + @Test public void statusCode() { StatusCodeException ex = new StatusCodeException(); 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()); + assertResolved(mav, 400, null); } @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()); + assertResolved(mav, 400, null); } @Test public void statusCodeAndReason() { StatusCodeAndReasonException ex = new StatusCodeAndReasonException(); ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); - assertNotNull("No ModelAndView returned", mav); - assertTrue("No Empty ModelAndView returned", mav.isEmpty()); - assertEquals("Invalid status code", 410, response.getStatus()); - assertEquals("Invalid status reason", "You suck!", response.getErrorMessage()); - assertTrue("Response has not been committed", response.isCommitted()); + assertResolved(mav, 410, "You suck!"); } @Test @@ -112,16 +106,29 @@ public class ResponseStatusExceptionResolverTests { assertNull("ModelAndView returned", mav); } - // SPR-12903 - - @Test + @Test // SPR-12903 public void nestedException() throws Exception { Exception cause = new StatusCodeAndReasonMessageException(); TypeMismatchException ex = new TypeMismatchException("value", ITestBean.class, cause); ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); - assertNotNull("No ModelAndView returned", mav); - assertTrue("No Empty ModelAndView returned", mav.isEmpty()); - assertEquals("Invalid status code", 410, response.getStatus()); + assertResolved(mav, 410, null); + } + + @Test + public void responseStatusException() throws Exception { + ResponseStatusException ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, "The reason"); + ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); + assertResolved(mav, 400, null); + } + + + private void assertResolved(ModelAndView mav, int status, String reason) { + assertTrue("No Empty ModelAndView returned", mav != null && mav.isEmpty()); + assertEquals(status, response.getStatus()); + if (reason != null) { + assertEquals(reason, response.getErrorMessage()); + } + assertTrue(response.isCommitted()); } @@ -142,6 +149,7 @@ public class ResponseStatusExceptionResolverTests { @ResponseStatus @Retention(RetentionPolicy.RUNTIME) + @SuppressWarnings("unused") @interface ComposedResponseStatus { @AliasFor(annotation = ResponseStatus.class, attribute = "code")