diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
index dbf71b14cf..ea3fbd0d84 100644
--- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
@@ -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.
*
- *
{@link ExceptionMethodMapping} is a key contributing class that stores method-to-exception mappings extracted
+ *
{@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.
*
*
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> messageConverters;
- private final Map, ExceptionMethodMapping> exceptionMethodMappingCache =
- new ConcurrentHashMap, ExceptionMethodMapping>();
+ private final Map, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
+ new ConcurrentHashMap, 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 (handlerMethod == null) {
+ return null;
+ }
+
+ ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
+ if (exceptionHandlerMethod == null) {
+ return null;
+ }
+
+ exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+ exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
- 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);
- }
+ ServletWebRequest webRequest = new ServletWebRequest(request, response);
+ ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- 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);
- }
+ 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;
}
- 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 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 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;
+ }
}
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java
index 8ef768be64..e0d1700c29 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java
@@ -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");
- }
-
- @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());
+ this.resolver = new ExceptionHandlerExceptionResolver();
+ this.resolver.afterPropertiesSet();
+ this.request = new MockHttpServletRequest("GET", "/");
+ this.response = new MockHttpServletResponse();
}
@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());
+ public void nullHandlerMethod() {
+ ModelAndView mav = this.resolver.resolveException(this.request, this.response, null, null);
+ assertNull(mav);
}
-
+
@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());
- }
+ 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);
- @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());
+ assertNull(mav);
}
@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 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(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 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 {
-
- @Override
- public String handleIOException(IOException ex, HttpServletRequest request) {
- return "GenericError";
- }
- }
-
-
@Controller
- private static class AmbiguousController {
+ static class ModelAndViewController {
- @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() {
+ }
+ }
+
}
\ No newline at end of file
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java
index 0a2510f4af..97c283b67a 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java
@@ -266,8 +266,6 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals(405, response.getStatus());
-
-
}
@Test
diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
new file mode 100644
index 0000000000..1445c5b695
--- /dev/null
+++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2011 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.method.annotation;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.core.ExceptionDepthComparator;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils.MethodFilter;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.method.HandlerMethodSelector;
+
+/**
+ * Given a set of @{@link ExceptionHandler} methods at initialization, finds
+ * the best matching method mapped to an exception at runtime.
+ *
+ * Exception mappings are extracted from the method @{@link ExceptionHandler}
+ * annotation or by looking for {@link Throwable} method arguments.
+ *
+ * @author Rossen Stoyanchev
+ * @since 3.1
+ */
+public class ExceptionHandlerMethodResolver {
+
+ private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis");
+
+ private final Map, Method> mappedMethods =
+ new ConcurrentHashMap, Method>();
+
+ private final Map, Method> exceptionLookupCache =
+ new ConcurrentHashMap, Method>();
+
+ /**
+ * A constructor that finds {@link ExceptionHandler} methods in a handler.
+ * @param handlerType the handler to inspect for exception handler methods.
+ * @throws IllegalStateException
+ * If an exception type is mapped to two methods.
+ * @throws IllegalArgumentException
+ * If an @{@link ExceptionHandler} method is not mapped to any exceptions.
+ */
+ public ExceptionHandlerMethodResolver(Class> handlerType) {
+ init(HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS));
+ }
+
+ private void init(Set exceptionHandlerMethods) {
+ for (Method method : exceptionHandlerMethods) {
+ for (Class extends Throwable> exceptionType : detectMappedExceptions(method)) {
+ addExceptionMapping(exceptionType, method);
+ }
+ }
+ }
+
+ /**
+ * Detect the exceptions an @{@link ExceptionHandler} method is mapped to.
+ * If the method @{@link ExceptionHandler} annotation doesn't have any,
+ * scan the method signature for all arguments of type {@link Throwable}.
+ */
+ @SuppressWarnings("unchecked")
+ private List> detectMappedExceptions(Method method) {
+ List> result = new ArrayList>();
+ ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
+ if (annotation != null) {
+ result.addAll(Arrays.asList(annotation.value()));
+ }
+ if (result.isEmpty()) {
+ for (Class> paramType : method.getParameterTypes()) {
+ if (Throwable.class.isAssignableFrom(paramType)) {
+ result.add((Class extends Throwable>) paramType);
+ }
+ }
+ }
+ Assert.notEmpty(result, "No exception types mapped to {" + method + "}");
+ return result;
+ }
+
+ private void addExceptionMapping(Class extends Throwable> exceptionType, Method method) {
+ Method oldMethod = this.mappedMethods.put(exceptionType, method);
+ if (oldMethod != null && !oldMethod.equals(method)) {
+ throw new IllegalStateException(
+ "Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" +
+ oldMethod + ", " + method + "}.");
+ }
+ }
+
+ /**
+ * Find a method to handle the given exception. If more than one match is
+ * found, the best match is selected via {@link ExceptionDepthComparator}.
+ * @param exception the exception
+ * @return an @{@link ExceptionHandler} method, or {@code null}
+ */
+ public Method resolveMethod(Exception exception) {
+ Class extends Exception> exceptionType = exception.getClass();
+ Method method = this.exceptionLookupCache.get(exceptionType);
+ if (method == null) {
+ method = getMappedMethod(exceptionType);
+ this.exceptionLookupCache.put(exceptionType, method != null ? method : NO_METHOD_FOUND);
+ }
+ return method != NO_METHOD_FOUND ? method : null;
+ }
+
+ /**
+ * Return the method mapped to the exception type, or {@code null}.
+ */
+ private Method getMappedMethod(Class extends Exception> exceptionType) {
+ List> matches = new ArrayList>();
+ for(Class extends Throwable> mappedException : this.mappedMethods.keySet()) {
+ if (mappedException.isAssignableFrom(exceptionType)) {
+ matches.add(mappedException);
+ }
+ }
+ if (!matches.isEmpty()) {
+ Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
+ return mappedMethods.get(matches.get(0));
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * A filter for selecting @{@link ExceptionHandler} methods.
+ */
+ public final static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() {
+
+ public boolean matches(Method method) {
+ return AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null;
+ }
+ };
+
+}
diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java
deleted file mode 100644
index 2ab1f796da..0000000000
--- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright 2002-2011 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.method.annotation;
-
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.springframework.core.ExceptionDepthComparator;
-import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-
-/**
- * Extracts and stores method-to-exception type mappings from a set of {@link ExceptionHandler}-annotated methods.
- * Subsequently {@link #getMethod(Exception)} can be used to matches an {@link Exception} to a method.
- *
- * Method-to-exception type mappings are usually derived from a method's {@link ExceptionHandler} annotation value.
- * The method argument list may also be checked for {@link Throwable} types if that's empty. Exception types can be
- * mapped to one method only.
- *
- *
When multiple exception types match a given exception, the best matching exception type is selected by sorting
- * the list of matches with {@link ExceptionDepthComparator}.
- *
- * @author Rossen Stoyanchev
- * @since 3.1
- */
-public class ExceptionMethodMapping {
-
- protected static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis");
-
- private final Map, Method> mappedExceptionTypes =
- new HashMap, Method>();
-
- private final Map, Method> resolvedExceptionTypes =
- new ConcurrentHashMap, Method>();
-
- /**
- * Creates an {@link ExceptionMethodMapping} instance from a set of {@link ExceptionHandler} methods.
- * While any {@link ExceptionHandler} methods can be provided, it is expected that the exception types
- * handled by any one method do not overlap with the exception types handled by any other method.
- * If two methods map to the same exception type, an exception is raised.
- * @param methods the {@link ExceptionHandler}-annotated methods to add to the mappings
- */
- public ExceptionMethodMapping(Set methods) {
- initExceptionMap(methods);
- }
-
- /**
- * Examines the provided methods and populates mapped exception types.
- */
- private void initExceptionMap(Set methods) {
- for (Method method : methods) {
- for (Class extends Throwable> exceptionType : getMappedExceptionTypes(method)) {
- Method prevMethod = mappedExceptionTypes.put(exceptionType, method);
-
- if (prevMethod != null && !prevMethod.equals(method)) {
- throw new IllegalStateException(
- "Ambiguous exception handler mapped for [" + exceptionType + "]: {" +
- prevMethod + ", " + method + "}.");
- }
- }
- }
- }
-
- /**
- * Derive the list of exception types mapped to the given method in one of the following ways:
- *
- * - The {@link ExceptionHandler} annotation value
- *
- {@link Throwable} types that appear in the method parameter list
- *
- * @param method the method to derive mapped exception types for
- * @return the list of exception types the method is mapped to, or an empty list
- */
- @SuppressWarnings("unchecked")
- protected List> getMappedExceptionTypes(Method method) {
- ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
- if (annotation.value().length != 0) {
- return Arrays.asList(annotation.value());
- }
- else {
- List> result = new ArrayList>();
- for (Class> paramType : method.getParameterTypes()) {
- if (Throwable.class.isAssignableFrom(paramType)) {
- result.add((Class extends Throwable>) paramType);
- }
- }
- Assert.notEmpty(result, "No exception types mapped to {" + method + "}");
- return result;
- }
- }
-
- /**
- * Get the {@link ExceptionHandler} method that matches the type of the provided {@link Exception}.
- * In case of multiple matches, the best match is selected with {@link ExceptionDepthComparator}.
- * @param exception the exception to find a matching {@link ExceptionHandler} method for
- * @return the mapped method, or {@code null} if none
- */
- public Method getMethod(Exception exception) {
- Class extends Exception> exceptionType = exception.getClass();
- Method method = resolvedExceptionTypes.get(exceptionType);
- if (method == null) {
- method = resolveExceptionType(exceptionType);
- resolvedExceptionTypes.put(exceptionType, method);
- }
- return (method != NO_METHOD_FOUND) ? method : null;
- }
-
- /**
- * Resolve the given exception type by iterating mapped exception types.
- * Uses {@link #getBestMatchingExceptionType(List, Class)} to select the best match.
- * @param exceptionType the exception type to resolve
- * @return the best matching method, or {@link ExceptionMethodMapping#NO_METHOD_FOUND}
- */
- protected final Method resolveExceptionType(Class extends Exception> exceptionType) {
- List> matches = new ArrayList>();
- for(Class extends Throwable> mappedExceptionType : mappedExceptionTypes.keySet()) {
- if (mappedExceptionType.isAssignableFrom(exceptionType)) {
- matches.add(mappedExceptionType);
- }
- }
- if (matches.isEmpty()) {
- return NO_METHOD_FOUND;
- }
- else {
- return mappedExceptionTypes.get(getBestMatchingExceptionType(matches, exceptionType));
- }
- }
-
- /**
- * Select the best match from the given list of exception types.
- */
- protected Class extends Throwable> getBestMatchingExceptionType(List> exceptionTypes,
- Class extends Exception> exceptionType) {
- Assert.isTrue(exceptionTypes.size() > 0, "No exception types to select from!");
- if (exceptionTypes.size() > 1) {
- Collections.sort(exceptionTypes, new ExceptionDepthComparator(exceptionType));
- }
- return exceptionTypes.get(0);
- }
-
-}
diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolverTests.java
new file mode 100644
index 0000000000..1a233bba9d
--- /dev/null
+++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolverTests.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2002-2011 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.method.annotation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.BindException;
+import java.net.SocketException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Test;
+import org.springframework.stereotype.Controller;
+import org.springframework.util.ClassUtils;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+/**
+ * Test fixture for {@link ExceptionHandlerMethodResolver} tests.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class ExceptionHandlerMethodResolverTests {
+
+ @Test
+ public void resolveMethodFromAnnotation() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
+ IOException exception = new IOException();
+ assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
+ }
+
+ @Test
+ public void resolveMethodFromArgument() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
+ IllegalArgumentException exception = new IllegalArgumentException();
+ assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName());
+ }
+
+ @Test
+ public void resolveMethodExceptionSubType() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
+ IOException ioException = new FileNotFoundException();
+ assertEquals("handleIOException", resolver.resolveMethod(ioException).getName());
+ SocketException bindException = new BindException();
+ assertEquals("handleSocketException", resolver.resolveMethod(bindException).getName());
+ }
+
+ @Test
+ public void resolveMethodBestMatch() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
+ SocketException exception = new SocketException();
+ assertEquals("handleSocketException", resolver.resolveMethod(exception).getName());
+ }
+
+ @Test
+ public void resolveMethodNoMatch() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
+ Exception exception = new Exception();
+ assertNull("1st lookup", resolver.resolveMethod(exception));
+ assertNull("2nd lookup from cache", resolver.resolveMethod(exception));
+ }
+
+ @Test
+ public void resolveMethodInherited() {
+ ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
+ IOException exception = new IOException();
+ assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void ambiguousExceptionMapping() {
+ new ExceptionHandlerMethodResolver(AmbiguousController.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void noExceptionMapping() {
+ new ExceptionHandlerMethodResolver(NoExceptionController.class);
+ }
+
+ @Controller
+ static class ExceptionController {
+
+ public void handle() {}
+
+ @ExceptionHandler(IOException.class)
+ public void handleIOException() {
+ }
+
+ @ExceptionHandler(SocketException.class)
+ public void handleSocketException() {
+ }
+
+ @ExceptionHandler
+ public void handleIllegalArgumentException(IllegalArgumentException exception) {
+ }
+ }
+
+ @Controller
+ static class InheritedController extends ExceptionController {
+
+ @Override
+ public void handleIOException() {
+ }
+ }
+
+ @Controller
+ static class AmbiguousController {
+
+ public void handle() {}
+
+ @ExceptionHandler({BindException.class, IllegalArgumentException.class})
+ public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ return ClassUtils.getShortName(ex.getClass());
+ }
+
+ @ExceptionHandler
+ public String handle2(IllegalArgumentException ex) {
+ return ClassUtils.getShortName(ex.getClass());
+ }
+ }
+
+ @Controller
+ static class NoExceptionController {
+
+ @ExceptionHandler
+ public void handle() {
+ }
+ }
+
+}