Browse Source

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

pull/7/head
Rossen Stoyanchev 13 years ago
parent
commit
91251812b1
  1. 133
      org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
  2. 191
      org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java
  3. 2
      org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java
  4. 153
      org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
  5. 163
      org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java
  6. 148
      org.springframework.web/src/test/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolverTests.java

133
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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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<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;
}
}

191
org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java

@ -17,33 +17,25 @@ @@ -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 @@ -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 { @@ -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() {
}
}
}

2
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 @@ -266,8 +266,6 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals(405, response.getStatus());
}
@Test

153
org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java

@ -0,0 +1,153 @@ @@ -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.
*
* <p>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<Class<? extends Throwable>, Method> mappedMethods =
new ConcurrentHashMap<Class<? extends Throwable>, Method>();
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache =
new ConcurrentHashMap<Class<? extends Throwable>, 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<Method> 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<Class<? extends Throwable>> detectMappedExceptions(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
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<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
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;
}
};
}

163
org.springframework.web/src/main/java/org/springframework/web/method/annotation/ExceptionMethodMapping.java

@ -1,163 +0,0 @@ @@ -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.
*
* <p>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.
*
* <p>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<Class<? extends Throwable>, Method> mappedExceptionTypes =
new HashMap<Class<? extends Throwable>, Method>();
private final Map<Class<? extends Throwable>, Method> resolvedExceptionTypes =
new ConcurrentHashMap<Class<? extends Throwable>, Method>();
/**
* Creates an {@link ExceptionMethodMapping} instance from a set of {@link ExceptionHandler} methods.
* <p>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<Method> methods) {
initExceptionMap(methods);
}
/**
* Examines the provided methods and populates mapped exception types.
*/
private void initExceptionMap(Set<Method> 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:
* <ol>
* <li>The {@link ExceptionHandler} annotation value
* <li>{@link Throwable} types that appear in the method parameter list
* </ol>
* @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<Class<? extends Throwable>> getMappedExceptionTypes(Method method) {
ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
if (annotation.value().length != 0) {
return Arrays.asList(annotation.value());
}
else {
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
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<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
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<Class<? extends Throwable>> 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);
}
}

148
org.springframework.web/src/test/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolverTests.java

@ -0,0 +1,148 @@ @@ -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() {
}
}
}
Loading…
Cancel
Save