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<Object>, which means it can be customized to
write error content to the body of the response.

Issue: SPR-9290
This commit is contained in:
Rossen Stoyanchev 2012-08-21 11:18:01 -04:00
parent 58daeea1e2
commit 1cf4a2facd
4 changed files with 747 additions and 70 deletions

View File

@ -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.
*
* <p>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<Object> 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<Object>(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<HttpMethod> mediaTypes = new HashSet<HttpMethod>();
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<MediaType> 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);
}
}

View File

@ -59,6 +59,8 @@ import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMeth
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.0 * @since 3.0
*
* @see org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerSupport
* @see #handleNoSuchRequestHandlingMethod * @see #handleNoSuchRequestHandlingMethod
* @see #handleHttpRequestMethodNotSupported * @see #handleHttpRequestMethodNotSupported
* @see #handleHttpMediaTypeNotSupported * @see #handleHttpMediaTypeNotSupported

View File

@ -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<Class<? extends Throwable>> 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<String> supported = Arrays.asList("POST", "DELETE");
Exception ex = new HttpRequestMethodNotSupportedException("GET", supported);
ResponseEntity<Object> responseEntity = testException(ex);
assertEquals(EnumSet.of(HttpMethod.POST, HttpMethod.DELETE), responseEntity.getHeaders().getAllow());
}
@Test
public void handleHttpMediaTypeNotSupported() {
List<MediaType> acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML);
Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable);
ResponseEntity<Object> 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<Object> testException(Exception ex) {
ResponseEntity<Object> 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";
}
}
}

View File

@ -3646,12 +3646,20 @@ public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData
is only a matter of implementing the is only a matter of implementing the
<literal>resolveException(Exception, Handler)</literal> method and <literal>resolveException(Exception, Handler)</literal> method and
returning a <classname>ModelAndView</classname>, you may also use the provided returning a <classname>ModelAndView</classname>, you may also use the provided
<classname>SimpleMappingExceptionResolver</classname>. This resolver <classname>SimpleMappingExceptionResolver</classname> or create
<interfacename>@ExceptionHandler</interfacename> methods.
The <classname>SimpleMappingExceptionResolver</classname>
enables you to take the class name of any exception that might be thrown 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 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 exception mapping feature from the Servlet API, but it is also possible
to implement more finely grained mappings of exceptions from different to implement more finely grained mappings of exceptions from different
handlers.</para> handlers. The <interfacename>@ExceptionHandler</interfacename> 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
<interfacename>@Controller</interfacename> or may apply globally to all
<interfacename>@RequestMapping</interfacename> methods when defined within
an <interfacename>@ControllerAdvice</interfacename> class.
The following sections explain this in more detail.</para>
</section> </section>
<section id="mvc-ann-exceptionhandler"> <section id="mvc-ann-exceptionhandler">
@ -3659,36 +3667,44 @@ public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData
<para>The <interfacename>HandlerExceptionResolver</interfacename> interface <para>The <interfacename>HandlerExceptionResolver</interfacename> interface
and the <classname>SimpleMappingExceptionResolver</classname> implementations and the <classname>SimpleMappingExceptionResolver</classname> implementations
allow you to map Exceptions to specific views along with some Java logic allow you to map Exceptions to specific views declaratively along with some
before forwarding to those views. However, in some cases, especially when optional Java logic before forwarding to those views. However, in some cases,
working with programmatic clients (Ajax or non-browser) it is more especially when relying on <interfacename>@ResponseBody</interfacename> methods
convenient to set the status and optionally write error information to the rather than on view resolution, it may be more convenient to directly set the
response body.</para> status of the response and optionally write error content to the body of the
response.</para>
<para>For that you can use <interfacename>@ExceptionHandler</interfacename> <para>You can do that with <interfacename>@ExceptionHandler</interfacename>
methods. When present within a controller such methods apply to exceptions methods. When declared within a controller such methods apply to exceptions
raised by that contoroller or any of its sub-classes. raised by <interfacename>@RequestMapping</interfacename> methods of that
Or you can also declare <interfacename>@ExceptionHandler</interfacename> contoroller (or any of its sub-classes). You can also declare an
methods in an <interfacename>@ControllerAdvice</interfacename>-annotated <interfacename>@ExceptionHandler</interfacename> method within an
class and such methods apply to any controller. <interfacename>@ControllerAdvice</interfacename> class in which case it
handles exceptions from <interfacename>@RequestMapping</interfacename>
methods from any controller.
The <interfacename>@ControllerAdvice</interfacename> annotation is The <interfacename>@ControllerAdvice</interfacename> annotation is
a component annotation allowing implementation classes to be autodetected a component annotation, which can be used with classpath scanning. It is
through classpath scanning. automatically enabled when using the MVC namespace and Java config, or
</para> otherwise depending on whether the
<classname>ExceptionHandlerExceptionResolver</classname> is configured or not.
<para>Here is an example with a controller-level Below is an example of a controller-local
<interfacename>@ExceptionHandler</interfacename> method:</para> <interfacename>@ExceptionHandler</interfacename> method:</para>
<programlisting language="java">@Controller <programlisting language="java">@Controller
public class SimpleController { public class SimpleController {
// other controller method omitted
// @RequestMapping methods omitted ...
@ExceptionHandler(IOException.class) @ExceptionHandler(IOException.class)
public ResponseEntity handleIOException(IOException ex) { public ResponseEntity&lt;String&gt; handleIOException(IOException ex) {
// prepare responseEntity // prepare responseEntity
return responseEntity; return responseEntity;
} }
}</programlisting> }</programlisting>
<para>The <classname>@ExceptionHandler</classname> value can be set to <para>The <classname>@ExceptionHandler</classname> value can be set to
@ -3711,30 +3727,25 @@ public class SimpleController {
<interfacename>@ResponseBody</interfacename> to have the method return value <interfacename>@ResponseBody</interfacename> to have the method return value
converted with message converters and written to the response stream.</para> converted with message converters and written to the response stream.</para>
<note><para>To better understand how <interfacename>@ExceptionHandler</interfacename>
methods work, consider that in Spring MVC there is only one abstraction
for handling exceptions and that's the
<interfacename>HandlerExceptionResolver</interfacename>. There is a special
implementation of that interface,
the <classname>ExceptionHandlerExceptionResolver</classname>, which detects
and invokes <interfacename>@ExceptionHandler</interfacename> methods.</para></note>
</section> </section>
<section id="mvc-ann-rest-spring-mvc-exceptions"> <section id="mvc-ann-rest-spring-mvc-exceptions">
<title>Handling of Spring MVC Exceptions</title> <title>Handling Standard Spring MVC Exceptions</title>
<para>Spring MVC may raise a number of exceptions while processing a request. <para>Spring MVC may raise a number of exceptions while processing
A <classname>SimpleMappingExceptionResolver</classname> can be used to easily a request. The <classname>SimpleMappingExceptionResolver</classname> can easily
map any exception to a default error view or to more specific error views if map any exception to a default error view as needed.
desired. However when responding to programmatic clients you may prefer to However, when working with clients that interpret responses in an automated
translate specific exceptions to the appropriate status that indicates a way you will want to set specific status code on the response. Depending on
client error (4xx) or a server error (5xx).</para> the exception raised the status code may indicate a client error (4xx) or a
server error (5xx).</para>
<para>For this reason Spring MVC provides the <para>The <classname>DefaultHandlerExceptionResolver</classname> translates
<classname>DefaultHandlerExceptionResolver</classname>, which translates specific Spring MVC exceptions to specific error status codes. It is registered
Spring MVC exceptions by setting a specific response status code. By default, by default with the MVC namespace, the MVC Java config. and also by the
this resolver is registered by the <classname>DispatcherServlet</classname>. the <classname>DispatcherServlet</classname> (i.e. when not using the MVC
The following table describes some of the exceptions it handles: namespace or Java config). Listed below are some of the exceptions handled
by this resolver and the corresponding status codes:
<informaltable> <informaltable>
<tgroup cols="2"> <tgroup cols="2">
<thead> <thead>
@ -3822,38 +3833,19 @@ public class SimpleController {
</informaltable> </informaltable>
</para> </para>
<note><para>If you explicitly register one or more <para>The <classname>DefaultHandlerExceptionResolver</classname> works
<interfacename>HandlerExceptionResolver</interfacename> instances in your configuration transparently by setting the status of the response. However, it stops short
then the defaults registered by the <classname>DispatcherServlet</classname> are of writing any error content to the body of the response while your
cancelled. This is standard behavior with regards to application may need to add developer-friendly content to every error
<classname>DispatcherServlet</classname> defaults. response for example when providing a REST API.</para>
See <xref linkend="mvc-servlet-special-bean-types"/> for more details.</para></note>
<para>If building a REST API, then it's very likely you will want to <para>To achieve this extend <classname>ExceptionHandlerSupport</classname>,
write some additional information about the error to the body of the response a convenient base class with an <interfacename>@ExceptionHandler</interfacename>
consistent with the API's error handling throughout. This includes the handling of method that handles standard Spring MVC exceptions just as the
Spring MVC exceptions, for which the <classname>DefaultHandlerExceptionResolver</classname> <classname>DefaultHandlerExceptionResolver</classname> does but also
only sets the status code and doesn't assume how or what content should be written allowing you to prepare error content for the body of the response.
to the body.</para> See the Javadoc of <classname>ExceptionHandlerSupport</classname>
for more details.</para>
<para>Instead you can create an <interfacename>@ControllerAdvice</interfacename>
class that handles each of the exceptions handled by the
<classname>DefaultHandlerExceptionResolver</classname> 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:</para>
<programlisting language="java">@ControllerAdvice
public class ApplicationExceptionResolver {
@ExceptionHandler
public ResponseEntity handleMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex) {
MyApiError error = ... ;
return new ResponseEntity(error, HttpStatus.SC_NOT_ACCEPTABLE);
}
// more @ExceptionHandler methods ...
}</programlisting>
</section> </section>
<section id="mvc-ann-annotated-exceptions"> <section id="mvc-ann-annotated-exceptions">
@ -3869,6 +3861,59 @@ public class ApplicationExceptionResolver {
</section> </section>
<section id="mvc-ann-customer-servlet-container-error-page">
<title>Customizing the Default Servlet Container Error Page</title>
<para>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 <code>&lt;error-page&gt;</code> element in
<filename>web.xml</filename>. 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.</para>
<programlisting language="xml">&lt;error-page&gt;
&lt;location&gt;/error&lt;/location&gt;
&lt;/error-page&gt;
</programlisting>
<para>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 <interfacename>@Controller</interfacename> method:</para>
<para>When writing error information, the status code and the error message
set on the <interfacename>HttpServletResponse</interfacename> can be
accessed through request attributes in a controller:</para>
<programlisting language="java">@Controller
public class ErrorController {
@RequestMapping(value="/error", produces="application/json")
@ResponseBody
public Map&lt;String, Object&gt; handle(HttpServletRequest request) {
Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}</programlisting>
<para>or in a JSP:</para>
<programlisting language="xml">&lt;%@ page contentType="application/json" pageEncoding="UTF-8"%&gt;
{
status:&lt;%=request.getAttribute("javax.servlet.error.status_code") %&gt;,
reason:&lt;%=request.getAttribute("javax.servlet.error.message") %&gt;
}</programlisting>
</section>
</section> </section>
<section id="mvc-coc"> <section id="mvc-coc">