Polish @ExceptionHandler method resolution. Allow subclasses to plug in additional @ExceptionHandler methods.
This commit is contained in:
parent
04bcd77520
commit
91251812b1
|
|
@ -20,7 +20,6 @@ import java.lang.reflect.Method;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
|
@ -28,20 +27,17 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import javax.xml.transform.Source;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.support.WebArgumentResolver;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.HandlerMethodSelector;
|
||||
import org.springframework.web.method.annotation.ExceptionMethodMapping;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
|
||||
import org.springframework.web.method.annotation.support.ModelAttributeMethodProcessor;
|
||||
import org.springframework.web.method.annotation.support.ModelMethodProcessor;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
|
|
@ -65,9 +61,9 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodR
|
|||
* An {@link AbstractHandlerMethodExceptionResolver} that supports using {@link ExceptionHandler}-annotated methods
|
||||
* to resolve exceptions.
|
||||
*
|
||||
* <p>{@link ExceptionMethodMapping} is a key contributing class that stores method-to-exception mappings extracted
|
||||
* <p>{@link ExceptionHandlerMethodResolver} is a key contributing class that stores method-to-exception mappings extracted
|
||||
* from {@link ExceptionHandler} annotations or from the list of method arguments on the exception-handling method.
|
||||
* {@link ExceptionMethodMapping} assists with actually locating a method for a thrown exception.
|
||||
* {@link ExceptionHandlerMethodResolver} assists with actually locating a method for a thrown exception.
|
||||
*
|
||||
* <p>Once located the invocation of the exception-handling method is done using much of the same classes
|
||||
* used for {@link RequestMapping} methods, which is described under {@link RequestMappingHandlerAdapter}.
|
||||
|
|
@ -87,8 +83,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
|||
|
||||
private List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
private final Map<Class<?>, ExceptionMethodMapping> exceptionMethodMappingCache =
|
||||
new ConcurrentHashMap<Class<?>, ExceptionMethodMapping>();
|
||||
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
|
||||
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
|
||||
|
||||
private HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
|
|
@ -205,90 +201,77 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
|||
}
|
||||
|
||||
/**
|
||||
* Attempts to find an {@link ExceptionHandler}-annotated method that can handle the thrown exception.
|
||||
* The exception-handling method, if found, is invoked resulting in a {@link ModelAndView}.
|
||||
* @return a {@link ModelAndView} if a matching exception-handling method was found, or {@code null} otherwise
|
||||
* Find an @{@link ExceptionHandler} method and invoke it to handle the
|
||||
* raised exception.
|
||||
*/
|
||||
@Override
|
||||
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
|
||||
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
HandlerMethod handlerMethod,
|
||||
HandlerMethod handlerMethod,
|
||||
Exception exception) {
|
||||
if (handlerMethod != null) {
|
||||
ExceptionMethodMapping mapping = getExceptionMethodMapping(handlerMethod);
|
||||
Method method = mapping.getMethod(exception);
|
||||
|
||||
if (method != null) {
|
||||
Object handler = handlerMethod.getBean();
|
||||
ServletInvocableHandlerMethod exceptionHandler = new ServletInvocableHandlerMethod(handler, method);
|
||||
exceptionHandler.setHandlerMethodArgumentResolvers(argumentResolvers);
|
||||
exceptionHandler.setHandlerMethodReturnValueHandlers(returnValueHandlers);
|
||||
|
||||
ServletWebRequest webRequest = new ServletWebRequest(request, response);
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Invoking exception-handling method: " + exceptionHandler);
|
||||
}
|
||||
|
||||
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||
|
||||
exceptionHandler.invokeAndHandle(webRequest, mavContainer, exception);
|
||||
|
||||
if (!mavContainer.isResolveView()) {
|
||||
return new ModelAndView();
|
||||
}
|
||||
else {
|
||||
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
|
||||
mav.setViewName(mavContainer.getViewName());
|
||||
if (!mavContainer.isViewReference()) {
|
||||
mav.setView((View) mavContainer.getView());
|
||||
}
|
||||
return mav;
|
||||
}
|
||||
}
|
||||
catch (Exception invocationEx) {
|
||||
logger.error("Invoking exception-handling method resulted in exception : " +
|
||||
exceptionHandler, invocationEx);
|
||||
}
|
||||
}
|
||||
if (handlerMethod == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
|
||||
if (exceptionHandlerMethod == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
|
||||
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
|
||||
|
||||
ServletWebRequest webRequest = new ServletWebRequest(request, response);
|
||||
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
|
||||
}
|
||||
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
|
||||
}
|
||||
catch (Exception invocationEx) {
|
||||
logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!mavContainer.isResolveView()) {
|
||||
return new ModelAndView();
|
||||
}
|
||||
else {
|
||||
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
|
||||
mav.setViewName(mavContainer.getViewName());
|
||||
if (!mavContainer.isViewReference()) {
|
||||
mav.setView((View) mavContainer.getView());
|
||||
}
|
||||
return mav;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an {@link ExceptionMethodMapping} for the the given handler method, never {@code null}
|
||||
* Find the @{@link ExceptionHandler} method for the given exception.
|
||||
* The default implementation searches @{@link ExceptionHandler} methods
|
||||
* in the class hierarchy of the method that raised the exception.
|
||||
* @param handlerMethod the method where the exception was raised
|
||||
* @param exception the raised exception
|
||||
* @return a method to handle the exception, or {@code null}
|
||||
*/
|
||||
private ExceptionMethodMapping getExceptionMethodMapping(HandlerMethod handlerMethod) {
|
||||
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
|
||||
Class<?> handlerType = handlerMethod.getBeanType();
|
||||
ExceptionMethodMapping mapping = exceptionMethodMappingCache.get(handlerType);
|
||||
if (mapping == null) {
|
||||
Set<Method> methods = HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS);
|
||||
extendExceptionHandlerMethods(methods, handlerType);
|
||||
mapping = new ExceptionMethodMapping(methods);
|
||||
exceptionMethodMappingCache.put(handlerType, mapping);
|
||||
}
|
||||
return mapping;
|
||||
Method method = getExceptionHandlerMethodResolver(handlerType).resolveMethod(exception);
|
||||
return (method != null) ? new ServletInvocableHandlerMethod(handlerMethod.getBean(), method) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension hook that subclasses can override to register additional @{@link ExceptionHandler} methods
|
||||
* by controller type. By default only @{@link ExceptionHandler} methods from the same controller are
|
||||
* included.
|
||||
* @param methods the list of @{@link ExceptionHandler} methods detected in the controller allowing to add more
|
||||
* @param handlerType the controller type to which the @{@link ExceptionHandler} methods will apply
|
||||
* Return a method resolver for the given handler type, never {@code null}.
|
||||
*/
|
||||
protected void extendExceptionHandlerMethods(Set<Method> methods, Class<?> handlerType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* MethodFilter that matches {@link ExceptionHandler @ExceptionHandler} methods.
|
||||
*/
|
||||
public static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() {
|
||||
|
||||
public boolean matches(Method method) {
|
||||
return AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null;
|
||||
private ExceptionHandlerMethodResolver getExceptionHandlerMethodResolver(Class<?> handlerType) {
|
||||
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerMethodResolvers.get(handlerType);
|
||||
if (resolver == null) {
|
||||
resolver = new ExceptionHandlerMethodResolver(handlerType);
|
||||
this.exceptionHandlerMethodResolvers.put(handlerType, resolver);
|
||||
}
|
||||
};
|
||||
return resolver;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,33 +17,25 @@
|
|||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.BindException;
|
||||
import java.net.SocketException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.support.InvocableHandlerMethod;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
|
||||
|
||||
/**
|
||||
* Test fixture with {@link ExceptionHandlerExceptionResolver}.
|
||||
|
|
@ -54,7 +46,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc
|
|||
*/
|
||||
public class ExceptionHandlerExceptionResolverTests {
|
||||
|
||||
private ExceptionHandlerExceptionResolver exceptionResolver;
|
||||
private ExceptionHandlerExceptionResolver resolver;
|
||||
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
|
|
@ -62,178 +54,101 @@ public class ExceptionHandlerExceptionResolverTests {
|
|||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
exceptionResolver = new ExceptionHandlerExceptionResolver();
|
||||
exceptionResolver.afterPropertiesSet();
|
||||
request = new MockHttpServletRequest();
|
||||
response = new MockHttpServletResponse();
|
||||
request.setMethod("GET");
|
||||
this.resolver = new ExceptionHandlerExceptionResolver();
|
||||
this.resolver.afterPropertiesSet();
|
||||
this.request = new MockHttpServletRequest("GET", "/");
|
||||
this.response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleWithIOException() throws NoSuchMethodException {
|
||||
IOException ex = new IOException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertEquals("Invalid view name returned", "X:IOException", mav.getViewName());
|
||||
assertEquals("Invalid status code returned", 500, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleWithSocketException() throws NoSuchMethodException {
|
||||
SocketException ex = new SocketException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertEquals("Invalid view name returned", "Y:SocketException", mav.getViewName());
|
||||
assertEquals("Invalid status code returned", 406, response.getStatus());
|
||||
assertEquals("Invalid status reason returned", "This is simply unacceptable!", response.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleWithFileNotFoundException() throws NoSuchMethodException {
|
||||
FileNotFoundException ex = new FileNotFoundException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertEquals("Invalid view name returned", "X:FileNotFoundException", mav.getViewName());
|
||||
assertEquals("Invalid status code returned", 500, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleWithBindException() throws NoSuchMethodException {
|
||||
BindException ex = new BindException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new SimpleController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertEquals("Invalid view name returned", "Y:BindException", mav.getViewName());
|
||||
assertEquals("Invalid status code returned", 406, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inherited() throws NoSuchMethodException {
|
||||
IOException ex = new IOException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new InheritedController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertEquals("Invalid view name returned", "GenericError", mav.getViewName());
|
||||
assertEquals("Invalid status code returned", 500, response.getStatus());
|
||||
public void nullHandlerMethod() {
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, null, null);
|
||||
assertNull(mav);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void ambiguous() throws NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new AmbiguousController(), "handle");
|
||||
exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
@Test
|
||||
public void noExceptionHandlerMethod() throws NoSuchMethodException {
|
||||
Exception exception = new NullPointerException();
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new IoExceptionController(), "handle");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, exception);
|
||||
|
||||
assertNull(mav);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modelAndViewController() throws NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException("Bad argument");
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new ModelAndViewController(), "handle");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
assertNotNull(mav);
|
||||
assertFalse(mav.isEmpty());
|
||||
assertEquals("errorView", mav.getViewName());
|
||||
assertEquals("Bad argument", mav.getModel().get("detail"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noModelAndView() throws UnsupportedEncodingException, NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new NoMAVReturningController(), "handle");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertTrue("ModelAndView not empty", mav.isEmpty());
|
||||
assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString());
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new NoModelAndViewController(), "handle");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
assertNotNull(mav);
|
||||
assertTrue(mav.isEmpty());
|
||||
assertEquals("IllegalArgumentException", this.response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseBody() throws UnsupportedEncodingException, NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new InvocableHandlerMethod(new ResponseBodyController(), "handle");
|
||||
request.addHeader("Accept", "text/plain");
|
||||
ModelAndView mav = exceptionResolver.resolveException(request, response, handlerMethod, ex);
|
||||
assertNotNull("No ModelAndView returned", mav);
|
||||
assertTrue("ModelAndView not empty", mav.isEmpty());
|
||||
assertEquals("Invalid response written", "IllegalArgumentException", response.getContentAsString());
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
private static class SimpleController {
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void handle() {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler(IOException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public String handleIOException(IOException ex, HttpServletRequest request) {
|
||||
return "X:" + ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler(SocketException.class)
|
||||
@ResponseStatus(value = HttpStatus.NOT_ACCEPTABLE, reason = "This is simply unacceptable!")
|
||||
public String handleSocketException(Exception ex, HttpServletResponse response) {
|
||||
return "Y:" + ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public String handleIllegalArgumentException(Exception ex) {
|
||||
return ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
assertNotNull(mav);
|
||||
assertTrue(mav.isEmpty());
|
||||
assertEquals("IllegalArgumentException", this.response.getContentAsString());
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
private static class InheritedController extends SimpleController {
|
||||
static class ModelAndViewController {
|
||||
|
||||
@Override
|
||||
public String handleIOException(IOException ex, HttpServletRequest request) {
|
||||
return "GenericError";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
private static class AmbiguousController {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void handle() {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler({BindException.class, IllegalArgumentException.class})
|
||||
public String handle1(Exception ex, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException {
|
||||
return ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler
|
||||
public String handle2(IllegalArgumentException ex) {
|
||||
return ClassUtils.getShortName(ex.getClass());
|
||||
public ModelAndView handle(Exception ex) throws IOException {
|
||||
return new ModelAndView("errorView", "detail", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Controller
|
||||
private static class NoMAVReturningController {
|
||||
static class NoModelAndViewController {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void handle() {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ExceptionHandler
|
||||
public void handle(Exception ex, Writer writer) throws IOException {
|
||||
writer.write(ClassUtils.getShortName(ex.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
private static class ResponseBodyController {
|
||||
static class ResponseBodyController {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void handle() {}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ExceptionHandler
|
||||
@ResponseBody
|
||||
public String handle(Exception ex) {
|
||||
return ClassUtils.getShortName(ex.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class IoExceptionController {
|
||||
|
||||
@ExceptionHandler(value=IOException.class)
|
||||
public void handle() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -266,8 +266,6 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
|
|||
response = new MockHttpServletResponse();
|
||||
getServlet().service(request, response);
|
||||
assertEquals(405, response.getStatus());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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…
Reference in New Issue