Polish @ExceptionHandler method resolution. Allow subclasses to plug in additional @ExceptionHandler methods.

This commit is contained in:
Rossen Stoyanchev
2011-09-02 11:04:23 +00:00
parent 04bcd77520
commit 91251812b1
6 changed files with 419 additions and 385 deletions

View File

@@ -20,7 +20,6 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
@@ -28,20 +27,17 @@ import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Source;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.method.annotation.ExceptionMethodMapping;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor;
import org.springframework.web.method.annotation.support.ModelMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -65,9 +61,9 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodR
* An {@link AbstractHandlerMethodExceptionResolver} that supports using {@link ExceptionHandler}-annotated methods
* to resolve exceptions.
*
* <p>{@link ExceptionMethodMapping} is a key contributing class that stores method-to-exception mappings extracted
* <p>{@link ExceptionHandlerMethodResolver} is a key contributing class that stores method-to-exception mappings extracted
* from {@link ExceptionHandler} annotations or from the list of method arguments on the exception-handling method.
* {@link ExceptionMethodMapping} assists with actually locating a method for a thrown exception.
* {@link ExceptionHandlerMethodResolver} assists with actually locating a method for a thrown exception.
*
* <p>Once located the invocation of the exception-handling method is done using much of the same classes
* used for {@link RequestMapping} methods, which is described under {@link RequestMappingHandlerAdapter}.
@@ -87,8 +83,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
private List<HttpMessageConverter<?>> messageConverters;
private final Map<Class<?>, ExceptionMethodMapping> exceptionMethodMappingCache =
new ConcurrentHashMap<Class<?>, ExceptionMethodMapping>();
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
private HandlerMethodArgumentResolverComposite argumentResolvers;
@@ -205,90 +201,77 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
/**
* Attempts to find an {@link ExceptionHandler}-annotated method that can handle the thrown exception.
* The exception-handling method, if found, is invoked resulting in a {@link ModelAndView}.
* @return a {@link ModelAndView} if a matching exception-handling method was found, or {@code null} otherwise
* Find an @{@link ExceptionHandler} method and invoke it to handle the
* raised exception.
*/
@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod,
HandlerMethod handlerMethod,
Exception exception) {
if (handlerMethod != null) {
ExceptionMethodMapping mapping = getExceptionMethodMapping(handlerMethod);
Method method = mapping.getMethod(exception);
if (method != null) {
Object handler = handlerMethod.getBean();
ServletInvocableHandlerMethod exceptionHandler = new ServletInvocableHandlerMethod(handler, method);
exceptionHandler.setHandlerMethodArgumentResolvers(argumentResolvers);
exceptionHandler.setHandlerMethodReturnValueHandlers(returnValueHandlers);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking exception-handling method: " + exceptionHandler);
}
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
exceptionHandler.invokeAndHandle(webRequest, mavContainer, exception);
if (!mavContainer.isResolveView()) {
return new ModelAndView();
}
else {
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
return mav;
}
}
catch (Exception invocationEx) {
logger.error("Invoking exception-handling method resulted in exception : " +
exceptionHandler, invocationEx);
}
}
if (handlerMethod == null) {
return null;
}
return null;
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
}
catch (Exception invocationEx) {
logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
return null;
}
if (!mavContainer.isResolveView()) {
return new ModelAndView();
}
else {
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
return mav;
}
}
/**
* @return an {@link ExceptionMethodMapping} for the the given handler method, never {@code null}
* Find the @{@link ExceptionHandler} method for the given exception.
* The default implementation searches @{@link ExceptionHandler} methods
* in the class hierarchy of the method that raised the exception.
* @param handlerMethod the method where the exception was raised
* @param exception the raised exception
* @return a method to handle the exception, or {@code null}
*/
private ExceptionMethodMapping getExceptionMethodMapping(HandlerMethod handlerMethod) {
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = handlerMethod.getBeanType();
ExceptionMethodMapping mapping = exceptionMethodMappingCache.get(handlerType);
if (mapping == null) {
Set<Method> methods = HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS);
extendExceptionHandlerMethods(methods, handlerType);
mapping = new ExceptionMethodMapping(methods);
exceptionMethodMappingCache.put(handlerType, mapping);
}
return mapping;
Method method = getExceptionHandlerMethodResolver(handlerType).resolveMethod(exception);
return (method != null) ? new ServletInvocableHandlerMethod(handlerMethod.getBean(), method) : null;
}
/**
* Extension hook that subclasses can override to register additional @{@link ExceptionHandler} methods
* by controller type. By default only @{@link ExceptionHandler} methods from the same controller are
* included.
* @param methods the list of @{@link ExceptionHandler} methods detected in the controller allowing to add more
* @param handlerType the controller type to which the @{@link ExceptionHandler} methods will apply
* Return a method resolver for the given handler type, never {@code null}.
*/
protected void extendExceptionHandlerMethods(Set<Method> methods, Class<?> handlerType) {
}
/**
* MethodFilter that matches {@link ExceptionHandler @ExceptionHandler} methods.
*/
public static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() {
public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null;
private ExceptionHandlerMethodResolver getExceptionHandlerMethodResolver(Class<?> handlerType) {
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerMethodResolvers.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerMethodResolvers.put(handlerType, resolver);
}
};
return resolver;
}
}

View File

@@ -17,33 +17,25 @@
package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.BindException;
import java.net.SocketException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
/**
* Test fixture with {@link ExceptionHandlerExceptionResolver}.
@@ -54,7 +46,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc
*/
public class ExceptionHandlerExceptionResolverTests {
private ExceptionHandlerExceptionResolver exceptionResolver;
private ExceptionHandlerExceptionResolver resolver;
private MockHttpServletRequest request;
@@ -62,178 +54,101 @@ public class ExceptionHandlerExceptionResolverTests {
@Before
public void setUp() throws Exception {
exceptionResolver = new ExceptionHandlerExceptionResolver();
exceptionResolver.afterPropertiesSet();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
request.setMethod("GET");
this.resolver = new ExceptionHandlerExceptionResolver();
this.resolver.afterPropertiesSet();
this.request = new MockHttpServletRequest("GET", "/");
this.response = new MockHttpServletResponse();
}
@Test
public void simpleWithIOException() throws NoSuchMethodException {
IOException ex = new IOException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "X:IOException", mav.getViewName());
assertEquals("Invalid status code returned", 500, response.getStatus());
}
@Test
public void simpleWithSocketException() throws NoSuchMethodException {
SocketException ex = new SocketException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "Y:SocketException", mav.getViewName());
assertEquals("Invalid status code returned", 406, response.getStatus());
assertEquals("Invalid status reason returned", "This is simply unacceptable!", response.getErrorMessage());
}
@Test
public void simpleWithFileNotFoundException() throws NoSuchMethodException {
FileNotFoundException ex = new FileNotFoundException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "X:FileNotFoundException", mav.getViewName());
assertEquals("Invalid status code returned", 500, response.getStatus());
}
@Test
public void simpleWithBindException() throws NoSuchMethodException {
BindException ex = new BindException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "Y:BindException", mav.getViewName());
assertEquals("Invalid status code returned", 406, response.getStatus());
}
@Test
public void inherited() throws NoSuchMethodException {
IOException ex = new IOException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new InheritedController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertEquals("Invalid view name returned", "GenericError", mav.getViewName());
assertEquals("Invalid status code returned", 500, response.getStatus());
public void nullHandlerMethod() {
ModelAndView mav = this.resolver.resolveException(this.request, this.response, null, null);
assertNull(mav);
}
@Test(expected = IllegalStateException.class)
public void ambiguous() throws NoSuchMethodException {
IllegalArgumentException ex = new IllegalArgumentException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new AmbiguousController(), "handle");
exceptionResolver.resolveException(request, response, handlerMethod, ex);
@Test
public void noExceptionHandlerMethod() throws NoSuchMethodException {
Exception exception = new NullPointerException();
HandlerMethod handlerMethod = new HandlerMethod(new IoExceptionController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, exception);
assertNull(mav);
}
@Test
public void modelAndViewController() throws NoSuchMethodException {
IllegalArgumentException ex = new IllegalArgumentException("Bad argument");
HandlerMethod handlerMethod = new HandlerMethod(new ModelAndViewController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
assertNotNull(mav);
assertFalse(mav.isEmpty());
assertEquals("errorView", mav.getViewName());
assertEquals("Bad argument", mav.getModel().get("detail"));
}
@Test
public void noModelAndView() throws UnsupportedEncodingException, NoSuchMethodException {
IllegalArgumentException ex = new IllegalArgumentException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new NoMAVReturningController(), "handle");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertTrue("ModelAndView not empty", mav.isEmpty());
assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString());
HandlerMethod handlerMethod = new HandlerMethod(new NoModelAndViewController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
assertNotNull(mav);
assertTrue(mav.isEmpty());
assertEquals("IllegalArgumentException", this.response.getContentAsString());
}
@Test
public void responseBody() throws UnsupportedEncodingException, NoSuchMethodException {
IllegalArgumentException ex = new IllegalArgumentException();
HandlerMethod handlerMethod = new InvocableHandlerMethod(new ResponseBodyController(), "handle");
request.addHeader("Accept", "text/plain");
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
assertNotNull("No ModelAndView returned", mav);
assertTrue("ModelAndView not empty", mav.isEmpty());
assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString());
}
@Controller
private static class SimpleController {
HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
@SuppressWarnings("unused")
public void handle() {}
@SuppressWarnings("unused")
@ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleIOException(IOException ex, HttpServletRequest request) {
return "X:" + ClassUtils.getShortName(ex.getClass());
}
@SuppressWarnings("unused")
@ExceptionHandler(SocketException.class)
@ResponseStatus(value = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!")
public String handleSocketException(Exception ex, HttpServletResponse response) {
return "Y:" + ClassUtils.getShortName(ex.getClass());
}
@SuppressWarnings("unused")
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(Exception ex) {
return ClassUtils.getShortName(ex.getClass());
}
assertNotNull(mav);
assertTrue(mav.isEmpty());
assertEquals("IllegalArgumentException", this.response.getContentAsString());
}
@Controller
private static class InheritedController extends SimpleController {
static class ModelAndViewController {
@Override
public String handleIOException(IOException ex, HttpServletRequest request) {
return "GenericError";
}
}
@Controller
private static class AmbiguousController {
@SuppressWarnings("unused")
public void handle() {}
@SuppressWarnings("unused")
@ExceptionHandler({BindException.class, IllegalArgumentException.class})
public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response)
throws IOException {
return ClassUtils.getShortName(ex.getClass());
}
@SuppressWarnings("unused")
@ExceptionHandler
public String handle2(IllegalArgumentException ex) {
return ClassUtils.getShortName(ex.getClass());
public ModelAndView handle(Exception ex) throws IOException {
return new ModelAndView("errorView", "detail", ex.getMessage());
}
}
@Controller
private static class NoMAVReturningController {
static class NoModelAndViewController {
@SuppressWarnings("unused")
public void handle() {}
@SuppressWarnings("unused")
@ExceptionHandler(Exception.class)
@ExceptionHandler
public void handle(Exception ex, Writer writer) throws IOException {
writer.write(ClassUtils.getShortName(ex.getClass()));
}
}
@Controller
private static class ResponseBodyController {
static class ResponseBodyController {
@SuppressWarnings("unused")
public void handle() {}
@SuppressWarnings("unused")
@ExceptionHandler(Exception.class)
@ExceptionHandler
@ResponseBody
public String handle(Exception ex) {
return ClassUtils.getShortName(ex.getClass());
}
}
@Controller
static class IoExceptionController {
@ExceptionHandler(value=IOException.class)
public void handle() {
}
}
}

View File

@@ -266,8 +266,6 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals(405, response.getStatus());
}
@Test