From 1cf4a2facd4fa0784fd10a767114011a79164abd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 21 Aug 2012 11:18:01 -0400 Subject: [PATCH] Add ExceptionHandlerSupport class The new class is functionally equivalent to the DefaultHandlerExceptionResolver (i.e. it translates Spring MVC exceptions to various status codes) but uses an @ExceptionHandler returning a ResponseEntity, which means it can be customized to write error content to the body of the response. Issue: SPR-9290 --- .../annotation/ExceptionHandlerSupport.java | 397 ++++++++++++++++++ .../DefaultHandlerExceptionResolver.java | 2 + .../ExceptionHandlerSupportTests.java | 233 ++++++++++ src/reference/docbook/mvc.xml | 185 +++++--- 4 files changed, 747 insertions(+), 70 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupport.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupportTests.java diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupport.java new file mode 100644 index 0000000000..f9345ee72b --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupport.java @@ -0,0 +1,397 @@ +/* + * 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.servlet.mvc.method.annotation; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.BindException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; + +/** + * A convenient base for classes with {@link ExceptionHandler} methods providing + * infrastructure to handle standard Spring MVC exceptions. The functionality is + * equivalent to that of the {@link DefaultHandlerExceptionResolver} except it + * can be customized to write error content to the body of the response. If there + * is no need to write error content, use {@code DefaultHandlerExceptionResolver} + * instead. + * + *

It is expected the sub-classes will be annotated with + * {@link ControllerAdvice @ControllerAdvice} and that + * {@link ExceptionHandlerExceptionResolver} is configured to ensure this class + * applies to exceptions from any {@link RequestMapping @RequestMapping} method. + * + * @author Rossen Stoyanchev + * @since 3.2 + * + * @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver + */ +public abstract class ExceptionHandlerSupport { + + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Log category to use when no mapped handler is found for a request. + * @see #pageNotFoundLogger + */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; + + /** + * Additional logger to use when no mapped handler is found for a request. + * @see #PAGE_NOT_FOUND_LOG_CATEGORY + */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + + /** + * Provides handling for standard Spring MVC exceptions. + * @param ex the target exception + * @param request the current request + */ + @ExceptionHandler(value={ + NoSuchRequestHandlingMethodException.class, + HttpRequestMethodNotSupportedException.class, + HttpMediaTypeNotSupportedException.class, + HttpMediaTypeNotAcceptableException.class, + MissingServletRequestParameterException.class, + ServletRequestBindingException.class, + ConversionNotSupportedException.class, + TypeMismatchException.class, + HttpMessageNotReadableException.class, + HttpMessageNotWritableException.class, + MethodArgumentNotValidException.class, + MissingServletRequestPartException.class, + BindException.class + }) + public final ResponseEntity handleException(Exception ex, WebRequest request) { + + HttpHeaders headers = new HttpHeaders(); + + HttpStatus status; + Object body; + + if (ex instanceof NoSuchRequestHandlingMethodException) { + status = HttpStatus.NOT_FOUND; + body = handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, headers, status, request); + } + else if (ex instanceof HttpRequestMethodNotSupportedException) { + status = HttpStatus.METHOD_NOT_ALLOWED; + body = handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request); + } + else if (ex instanceof HttpMediaTypeNotSupportedException) { + status = HttpStatus.UNSUPPORTED_MEDIA_TYPE; + body = handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException) { + status = HttpStatus.NOT_ACCEPTABLE; + body = handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request); + } + else if (ex instanceof MissingServletRequestParameterException) { + status = HttpStatus.BAD_REQUEST; + body = handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request); + } + else if (ex instanceof ServletRequestBindingException) { + status = HttpStatus.BAD_REQUEST; + body = handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request); + } + else if (ex instanceof ConversionNotSupportedException) { + status = HttpStatus.INTERNAL_SERVER_ERROR; + body = handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request); + } + else if (ex instanceof TypeMismatchException) { + status = HttpStatus.BAD_REQUEST; + body = handleTypeMismatch((TypeMismatchException) ex, headers, status, request); + } + else if (ex instanceof HttpMessageNotReadableException) { + status = HttpStatus.BAD_REQUEST; + body = handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request); + } + else if (ex instanceof HttpMessageNotWritableException) { + status = HttpStatus.INTERNAL_SERVER_ERROR; + body = handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request); + } + else if (ex instanceof MethodArgumentNotValidException) { + status = HttpStatus.BAD_REQUEST; + body = handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request); + } + else if (ex instanceof MissingServletRequestPartException) { + status = HttpStatus.BAD_REQUEST; + body = handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request); + } + else if (ex instanceof BindException) { + status = HttpStatus.BAD_REQUEST; + body = handleBindException((BindException) ex, headers, status, request); + } + else { + logger.warn("Unknown exception type: " + ex.getClass().getName()); + status = HttpStatus.INTERNAL_SERVER_ERROR; + body = handleExceptionInternal(ex, headers, status, request); + } + + return new ResponseEntity(body, headers, status); + } + + /** + * A single place to customize the response body of all Exception types. + * This method returns {@code null} by default. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + */ + protected Object handleExceptionInternal(Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + return null; + } + + /** + * Customize the response for NoSuchRequestHandlingMethodException. + * This method logs a warning and delegates to + * {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleNoSuchRequestHandlingMethod(NoSuchRequestHandlingMethodException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + pageNotFoundLogger.warn(ex.getMessage()); + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for HttpRequestMethodNotSupportedException. + * This method logs a warning, sets the "Allow" header, and delegates to + * {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + pageNotFoundLogger.warn(ex.getMessage()); + + Set mediaTypes = new HashSet(); + for (String value : ex.getSupportedMethods()) { + mediaTypes.add(HttpMethod.valueOf(value)); + } + if (!mediaTypes.isEmpty()) { + headers.setAllow(mediaTypes); + } + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for HttpMediaTypeNotSupportedException. + * This method sets the "Accept" header and delegates to + * {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + List mediaTypes = ex.getSupportedMediaTypes(); + if (!CollectionUtils.isEmpty(mediaTypes)) { + headers.setAccept(mediaTypes); + } + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for HttpMediaTypeNotAcceptableException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for MissingServletRequestParameterException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleMissingServletRequestParameter(MissingServletRequestParameterException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for ServletRequestBindingException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleServletRequestBindingException(ServletRequestBindingException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for ConversionNotSupportedException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleConversionNotSupported(ConversionNotSupportedException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for TypeMismatchException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for HttpMessageNotReadableException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for HttpMessageNotWritableException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleHttpMessageNotWritable(HttpMessageNotWritableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for MethodArgumentNotValidException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for MissingServletRequestPartException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleMissingServletRequestPart(MissingServletRequestPartException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + + /** + * Customize the response for BindException. + * This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return an Object or {@code null} + */ + protected Object handleBindException(BindException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, headers, status, request); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index b597f9e49d..104d9da351 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -59,6 +59,8 @@ import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMeth * @author Arjen Poutsma * @author Rossen Stoyanchev * @since 3.0 + * + * @see org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerSupport * @see #handleNoSuchRequestHandlingMethod * @see #handleHttpRequestMethodNotSupported * @see #handleHttpMediaTypeNotSupported diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupportTests.java new file mode 100644 index 0000000000..04d28018cf --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerSupportTests.java @@ -0,0 +1,233 @@ +/* + * 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.servlet.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.validation.BindException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; +import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; + +/** + * Test fixture for {@link ExceptionHandlerSupport}. + * + * @author Rossen Stoyanchev + */ +public class ExceptionHandlerSupportTests { + + private ExceptionHandlerSupport exceptionHandlerSupport; + + private DefaultHandlerExceptionResolver defaultExceptionResolver; + + private WebRequest request; + + private HttpServletRequest servletRequest; + + private MockHttpServletResponse servletResponse; + + + @Before + public void setup() { + this.servletRequest = new MockHttpServletRequest(); + this.servletResponse = new MockHttpServletResponse(); + this.request = new ServletWebRequest(this.servletRequest, this.servletResponse); + + this.exceptionHandlerSupport = new ApplicationExceptionHandler(); + this.defaultExceptionResolver = new DefaultHandlerExceptionResolver(); + } + + @Test + public void supportsAllDefaultHandlerExceptionResolverExceptionTypes() throws Exception { + + Method annotMethod = ExceptionHandlerSupport.class.getMethod("handleException", Exception.class, WebRequest.class); + ExceptionHandler annot = annotMethod.getAnnotation(ExceptionHandler.class); + List> supportedTypes = Arrays.asList(annot.value()); + + for (Method method : DefaultHandlerExceptionResolver.class.getDeclaredMethods()) { + Class[] paramTypes = method.getParameterTypes(); + if (method.getName().startsWith("handle") && (paramTypes.length == 4)) { + String name = paramTypes[0].getSimpleName(); + assertTrue("@ExceptionHandler is missing " + name, supportedTypes.contains(paramTypes[0])); + } + } + } + + @Test + public void noSuchRequestHandlingMethod() { + Exception ex = new NoSuchRequestHandlingMethodException("GET", TestController.class); + testException(ex); + } + + @Test + public void httpRequestMethodNotSupported() { + List supported = Arrays.asList("POST", "DELETE"); + Exception ex = new HttpRequestMethodNotSupportedException("GET", supported); + + ResponseEntity responseEntity = testException(ex); + assertEquals(EnumSet.of(HttpMethod.POST, HttpMethod.DELETE), responseEntity.getHeaders().getAllow()); + + } + + @Test + public void handleHttpMediaTypeNotSupported() { + List acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML); + Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable); + + ResponseEntity responseEntity = testException(ex); + assertEquals(acceptable, responseEntity.getHeaders().getAccept()); + } + + @Test + public void httpMediaTypeNotAcceptable() { + Exception ex = new HttpMediaTypeNotAcceptableException(""); + testException(ex); + } + + @Test + public void missingServletRequestParameter() { + Exception ex = new MissingServletRequestParameterException("param", "type"); + testException(ex); + } + + @Test + public void servletRequestBindingException() { + Exception ex = new ServletRequestBindingException("message"); + testException(ex); + } + + @Test + public void conversionNotSupported() { + Exception ex = new ConversionNotSupportedException(new Object(), Object.class, null); + testException(ex); + } + + @Test + public void typeMismatch() { + Exception ex = new TypeMismatchException("foo", String.class); + testException(ex); + } + + @Test + public void httpMessageNotReadable() { + Exception ex = new HttpMessageNotReadableException("message"); + testException(ex); + } + + @Test + public void httpMessageNotWritable() { + Exception ex = new HttpMessageNotWritableException(""); + testException(ex); + } + + @Test + public void methodArgumentNotValid() { + Exception ex = new MethodArgumentNotValidException(null, null); + testException(ex); + } + + @Test + public void missingServletRequestPart() { + Exception ex = new MissingServletRequestPartException("partName"); + testException(ex); + } + + @Test + public void bindException() { + Exception ex = new BindException(new Object(), "name"); + testException(ex); + } + + @Test + public void controllerAdvice() throws Exception { + StaticWebApplicationContext cxt = new StaticWebApplicationContext(); + cxt.registerSingleton("exceptionHandler", ApplicationExceptionHandler.class); + cxt.refresh(); + + ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver(); + resolver.setApplicationContext(cxt); + resolver.afterPropertiesSet(); + + ServletRequestBindingException ex = new ServletRequestBindingException("message"); + resolver.resolveException(this.servletRequest, this.servletResponse, null, ex); + + assertEquals(400, this.servletResponse.getStatus()); + assertEquals("error content", this.servletResponse.getContentAsString()); + assertEquals("someHeaderValue", this.servletResponse.getHeader("someHeader")); + } + + + private ResponseEntity testException(Exception ex) { + ResponseEntity responseEntity = this.exceptionHandlerSupport.handleException(ex, this.request); + this.defaultExceptionResolver.resolveException(this.servletRequest, this.servletResponse, null, ex); + + assertEquals(this.servletResponse.getStatus(), responseEntity.getStatusCode().value()); + + return responseEntity; + } + + + private static class TestController { + } + + @ControllerAdvice + private static class ApplicationExceptionHandler extends ExceptionHandlerSupport { + + @Override + protected Object handleServletRequestBindingException(ServletRequestBindingException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + headers.set("someHeader", "someHeaderValue"); + return "error content"; + } + + + } + +} diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 33b2c895cb..7868013ccb 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -3646,12 +3646,20 @@ public String onSubmit(@RequestPart("meta-data") MetaData is only a matter of implementing the resolveException(Exception, Handler) method and returning a ModelAndView, you may also use the provided - SimpleMappingExceptionResolver. This resolver + SimpleMappingExceptionResolver or create + @ExceptionHandler methods. + The SimpleMappingExceptionResolver enables you to take the class name of any exception that might be thrown and map it to a view name. This is functionally equivalent to the exception mapping feature from the Servlet API, but it is also possible to implement more finely grained mappings of exceptions from different - handlers. + handlers. The @ExceptionHandler annotation on + the other hand can be used on methods that should be invoked to handle an + exception. Such methods may be defined locally within an + @Controller or may apply globally to all + @RequestMapping methods when defined within + an @ControllerAdvice class. + The following sections explain this in more detail.
@@ -3659,36 +3667,44 @@ public String onSubmit(@RequestPart("meta-data") MetaData The HandlerExceptionResolver interface and the SimpleMappingExceptionResolver implementations - allow you to map Exceptions to specific views along with some Java logic - before forwarding to those views. However, in some cases, especially when - working with programmatic clients (Ajax or non-browser) it is more - convenient to set the status and optionally write error information to the - response body. + allow you to map Exceptions to specific views declaratively along with some + optional Java logic before forwarding to those views. However, in some cases, + especially when relying on @ResponseBody methods + rather than on view resolution, it may be more convenient to directly set the + status of the response and optionally write error content to the body of the + response. - For that you can use @ExceptionHandler - methods. When present within a controller such methods apply to exceptions - raised by that contoroller or any of its sub-classes. - Or you can also declare @ExceptionHandler - methods in an @ControllerAdvice-annotated - class and such methods apply to any controller. + You can do that with @ExceptionHandler + methods. When declared within a controller such methods apply to exceptions + raised by @RequestMapping methods of that + contoroller (or any of its sub-classes). You can also declare an + @ExceptionHandler method within an + @ControllerAdvice class in which case it + handles exceptions from @RequestMapping + methods from any controller. The @ControllerAdvice annotation is - a component annotation allowing implementation classes to be autodetected - through classpath scanning. - - - Here is an example with a controller-level + a component annotation, which can be used with classpath scanning. It is + automatically enabled when using the MVC namespace and Java config, or + otherwise depending on whether the + ExceptionHandlerExceptionResolver is configured or not. + Below is an example of a controller-local @ExceptionHandler method: @Controller public class SimpleController { - // other controller method omitted + + // @RequestMapping methods omitted ... + @ExceptionHandler(IOException.class) - public ResponseEntity handleIOException(IOException ex) { + public ResponseEntity<String> handleIOException(IOException ex) { + // prepare responseEntity + return responseEntity; } + } The @ExceptionHandler value can be set to @@ -3711,30 +3727,25 @@ public class SimpleController { @ResponseBody to have the method return value converted with message converters and written to the response stream. - To better understand how @ExceptionHandler - methods work, consider that in Spring MVC there is only one abstraction - for handling exceptions and that's the - HandlerExceptionResolver. There is a special - implementation of that interface, - the ExceptionHandlerExceptionResolver, which detects - and invokes @ExceptionHandler methods.
- Handling of Spring MVC Exceptions + Handling Standard Spring MVC Exceptions - Spring MVC may raise a number of exceptions while processing a request. - A SimpleMappingExceptionResolver can be used to easily - map any exception to a default error view or to more specific error views if - desired. However when responding to programmatic clients you may prefer to - translate specific exceptions to the appropriate status that indicates a - client error (4xx) or a server error (5xx). + Spring MVC may raise a number of exceptions while processing + a request. The SimpleMappingExceptionResolver can easily + map any exception to a default error view as needed. + However, when working with clients that interpret responses in an automated + way you will want to set specific status code on the response. Depending on + the exception raised the status code may indicate a client error (4xx) or a + server error (5xx). - For this reason Spring MVC provides the - DefaultHandlerExceptionResolver, which translates specific - Spring MVC exceptions by setting a specific response status code. By default, - this resolver is registered by the DispatcherServlet. - The following table describes some of the exceptions it handles: + The DefaultHandlerExceptionResolver translates + Spring MVC exceptions to specific error status codes. It is registered + by default with the MVC namespace, the MVC Java config. and also by the + the DispatcherServlet (i.e. when not using the MVC + namespace or Java config). Listed below are some of the exceptions handled + by this resolver and the corresponding status codes: @@ -3822,38 +3833,19 @@ public class SimpleController { - If you explicitly register one or more - HandlerExceptionResolver instances in your configuration - then the defaults registered by the DispatcherServlet are - cancelled. This is standard behavior with regards to - DispatcherServlet defaults. - See for more details. + The DefaultHandlerExceptionResolver works + transparently by setting the status of the response. However, it stops short + of writing any error content to the body of the response while your + application may need to add developer-friendly content to every error + response for example when providing a REST API. - If building a REST API, then it's very likely you will want to - write some additional information about the error to the body of the response - consistent with the API's error handling throughout. This includes the handling of - Spring MVC exceptions, for which the DefaultHandlerExceptionResolver - only sets the status code and doesn't assume how or what content should be written - to the body. - - Instead you can create an @ControllerAdvice - class that handles each of the exceptions handled by the - DefaultHandlerExceptionResolver while also writing - developer-friendly API error information to the response body consistent with - the rest of all API error handling of the application. For example: - - @ControllerAdvice -public class ApplicationExceptionResolver { - - @ExceptionHandler - public ResponseEntity handleMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex) { - MyApiError error = ... ; - return new ResponseEntity(error, HttpStatus.SC_NOT_ACCEPTABLE); - } - - // more @ExceptionHandler methods ... - -} + To achieve this extend ExceptionHandlerSupport, + a convenient base class with an @ExceptionHandler + method that handles standard Spring MVC exceptions just as the + DefaultHandlerExceptionResolver does but also + allowing you to prepare error content for the body of the response. + See the Javadoc of ExceptionHandlerSupport + for more details.
@@ -3869,6 +3861,59 @@ public class ApplicationExceptionResolver {
+
+ Customizing the Default Servlet Container Error Page + + When the status of the response is set to an error status code + and the body of the response is empty, Servlet containers commonly render + an HTML formatted error page. + To customize the default error page of the container, you can + declare an <error-page> element in + web.xml. Up until Servlet 3, that element had to + be mapped to a specific status code or exception type. Starting with + Servlet 3 an error page does not need to be mapped, which effectively + means the specified location customizes the default Servlet container + error page. + + <error-page> + <location>/error</location> +</error-page> + + + Note that the actual location for the error page can be a + JSP page or some other URL within the container including one handled + through an @Controller method: + + When writing error information, the status code and the error message + set on the HttpServletResponse can be + accessed through request attributes in a controller: + +@Controller +public class ErrorController { + + @RequestMapping(value="/error", produces="application/json") + @ResponseBody + public Map<String, Object> handle(HttpServletRequest request) { + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("status", request.getAttribute("javax.servlet.error.status_code")); + map.put("reason", request.getAttribute("javax.servlet.error.message")); + + return map; + } + +} + + or in a JSP: + + <%@ page contentType="application/json" pageEncoding="UTF-8"%> +{ + status:<%=request.getAttribute("javax.servlet.error.status_code") %>, + reason:<%=request.getAttribute("javax.servlet.error.message") %> +} + +
+