diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java index aa98634533..ef9d81f20f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.Principal; @@ -33,6 +34,7 @@ import java.util.Locale; import java.util.Map; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -46,6 +48,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.support.WebArgumentResolver; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -54,11 +57,23 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; import org.springframework.web.servlet.support.RequestContextUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; /** * Implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver} interface that handles - * exceptions through the {@link ExceptionHandler} annotation.

This exception resolver is enabled by default in the - * {@link org.springframework.web.servlet.DispatcherServlet}. + * exceptions through the {@link ExceptionHandler} annotation. + * + *

This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}. * * @author Arjen Poutsma * @since 3.0 @@ -67,6 +82,11 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc private WebArgumentResolver[] customArgumentResolvers; + private HttpMessageConverter[] messageConverters = + new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), + new FormHttpMessageConverter(), new SourceHttpMessageConverter()}; + + /** * Set a custom ArgumentResolvers to use for special method parameter types. Such a custom ArgumentResolver will kick * in first, having a chance to resolve an argument value before the standard argument handling kicks in. @@ -83,6 +103,14 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc this.customArgumentResolvers = argumentResolvers; } + /** + * Set the message body converters to use. + *

These converters are used to convert from and to HTTP requests and responses. + */ + public void setMessageConverters(HttpMessageConverter[] messageConverters) { + this.messageConverters = messageConverters; + } + @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @@ -331,6 +359,11 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc HttpServletResponse response = webRequest.getResponse(); response.setStatus(responseStatus.value().value()); } + + if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) { + return handleResponseBody(returnValue, webRequest); + } + if (returnValue instanceof ModelAndView) { return (ModelAndView) returnValue; } @@ -354,6 +387,36 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc } } + @SuppressWarnings("unchecked") + private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest) + throws ServletException, IOException { + + HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest()); + List acceptedMediaTypes = inputMessage.getHeaders().getAccept(); + if (acceptedMediaTypes.isEmpty()) { + acceptedMediaTypes = Collections.singletonList(MediaType.ALL); + } + MediaType.sortBySpecificity(acceptedMediaTypes); + HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); + Class returnValueType = returnValue.getClass(); + if (messageConverters != null) { + for (MediaType acceptedMediaType : acceptedMediaTypes) { + for (HttpMessageConverter messageConverter : messageConverters) { + if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { + messageConverter.write(returnValue, acceptedMediaType, outputMessage); + return new ModelAndView(); + } + } + } + } + if (logger.isWarnEnabled()) { + logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " + + acceptedMediaTypes); + } + return null; + } + + /** Comparator capable of sorting exceptions based on their depth from the thrown exception type. */ private static class DepthComparator implements Comparator> { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java index a4d65f2ea7..d9685143e6 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolverTests.java @@ -34,6 +34,7 @@ import org.springframework.stereotype.Controller; import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; /** @@ -91,6 +92,17 @@ public class AnnotationMethodHandlerExceptionResolverTests { assertTrue("ModelAndView not empty", mav.isEmpty()); assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString()); } + + @Test + public void responseBody() throws UnsupportedEncodingException { + IllegalArgumentException ex = new IllegalArgumentException(); + ResponseBodyController controller = new ResponseBodyController(); + request.addHeader("Accept", "text/plain"); + ModelAndView mav = exceptionResolver.resolveException(request, response, controller, ex); + assertNotNull("No ModelAndView returned", mav); + assertTrue("ModelAndView not empty", mav.isEmpty()); + assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString()); + } @Controller private static class SimpleController { @@ -147,4 +159,14 @@ public class AnnotationMethodHandlerExceptionResolverTests { writer.write(ClassUtils.getShortName(ex.getClass())); } } + + @Controller + private static class ResponseBodyController { + + @ExceptionHandler(Exception.class) + @ResponseBody + public String handle(Exception ex) { + return ClassUtils.getShortName(ex.getClass()); + } + } }