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 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) paramType); + } + } + } + Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); + return result; + } + + private void addExceptionMapping(Class 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 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 exceptionType) { + List> matches = new ArrayList>(); + for(Class 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 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: - *

    - *
  1. The {@link ExceptionHandler} annotation value - *
  2. {@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) 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 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 exceptionType) { - List> matches = new ArrayList>(); - for(Class 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 getBestMatchingExceptionType(List> exceptionTypes, - Class 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() { + } + } + +}