Add support for global @ExceptionHandler methods

Before this change @ExceptionHandler methods could be located in and
apply locally within a controller. The change makes it possible to have
such methods applicable globally regardless of the controller that
raised the exception.

The easiest way to do that is to add them to a class annotated with
`@ExceptionResolver`, a new annotation that is also an `@Component`
annotation (and therefore works with component scanning). It is also
possible to register classes containing `@ExceptionHandler` methods
directly with the ExceptionHandlerExceptionResolver.

When multiple `@ExceptionResolver` classes are detected, or registered
directly, the order in which they're used depends on the the `@Order`
annotation (if present) or on the value of the order field (if the
Ordered interface is implemented).

Issue: SPR-9112
This commit is contained in:
Rossen Stoyanchev 2012-06-11 14:34:44 -04:00
parent ccd2da37ce
commit c846198e46
6 changed files with 334 additions and 74 deletions

View File

@ -76,6 +76,15 @@ import java.lang.annotation.Target;
* {@link org.springframework.web.servlet.RequestToViewNameTranslator}.
* <li>A {@link org.springframework.web.servlet.View} object.
* <li>A {@link java.lang.String} value which is interpreted as view name.
* <li>{@link ResponseBody @ResponseBody} annotated methods (Servlet-only)
* to set the response content. The return value will be converted to the
* response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
* <li>An {@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
* {@link org.springframework.http.ResponseEntity ResponseEntity&lt;?&gt;} object
* (Servlet-only) to set response headers and content. The ResponseEntity body
* will be converted and written to the response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
* <li><code>void</code> if the method handles the response itself (by
* writing the response content directly, declaring an argument of type
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2002-2012 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.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* An {@linkplain Component @Component} annotation that indicates the annotated class
* contains {@linkplain ExceptionHandler @ExceptionHandler} methods. Such methods
* will be used in addition to {@code @ExceptionHandler} methods in
* {@code @Controller}-annotated classes.
*
* <p>In order for the the annotation to detected, an instance of
* {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}
* is configured.
*
* <p>Classes with this annotation may use the {@linkplain Order @Order} annotation
* or implement the {@link Ordered} interface to indicate the order in which they
* should be used relative to other such annotated components. However, note that
* the order is only for components registered through {@code @ExceptionResolver},
* i.e. within an
* {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}.
*
* @author Rossen Stoyanchev
* @since 3.2
*
* @see org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ExceptionResolver {
}

View File

@ -18,15 +18,23 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Source;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
@ -34,6 +42,7 @@ import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ExceptionResolver;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
@ -49,6 +58,8 @@ import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
import edu.emory.mathcs.backport.java.util.Collections;
/**
* An {@link AbstractHandlerMethodExceptionResolver} that resolves exceptions
* through {@code @ExceptionHandler} methods.
@ -62,7 +73,7 @@ import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionRes
* @since 3.1
*/
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements
InitializingBean {
InitializingBean, ApplicationContextAware {
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
@ -72,13 +83,18 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlersByType =
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
private final Map<Object, ExceptionHandlerMethodResolver> globalExceptionHandlers =
new LinkedHashMap<Object, ExceptionHandlerMethodResolver>();
private HandlerMethodArgumentResolverComposite argumentResolvers;
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
private ApplicationContext applicationContext;
/**
* Default constructor.
*/
@ -193,6 +209,22 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* Provide instances of objects with {@link ExceptionHandler @ExceptionHandler}
* methods to apply globally, i.e. regardless of the selected controller.
* <p>{@code @ExceptionHandler} methods in the controller are always looked
* up before {@code @ExceptionHandler} methods in global handlers.
*/
public void setGlobalExceptionHandlers(Object... handlers) {
for (Object handler : handlers) {
this.globalExceptionHandlers.put(handler, new ExceptionHandlerMethodResolver(handler.getClass()));
}
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void afterPropertiesSet() {
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
@ -202,6 +234,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
initGlobalExceptionHandlers();
}
/**
@ -255,6 +288,36 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
return handlers;
}
private void initGlobalExceptionHandlers() {
if (this.applicationContext == null) {
logger.warn("Can't detect @ExceptionResolver components if the ApplicationContext property is not set");
}
else {
String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);
for (String name : beanNames) {
Class<?> type = this.applicationContext.getType(name);
if (AnnotationUtils.findAnnotation(type , ExceptionResolver.class) != null) {
Object bean = this.applicationContext.getBean(name);
this.globalExceptionHandlers.put(bean, new ExceptionHandlerMethodResolver(bean.getClass()));
}
}
}
if (this.globalExceptionHandlers.size() > 0) {
sortGlobalExceptionHandlers();
}
}
private void sortGlobalExceptionHandlers() {
Map<Object, ExceptionHandlerMethodResolver> handlersCopy =
new HashMap<Object, ExceptionHandlerMethodResolver>(this.globalExceptionHandlers);
List<Object> handlers = new ArrayList<Object>(handlersCopy.keySet());
Collections.sort(handlers, new AnnotationAwareOrderComparator());
this.globalExceptionHandlers.clear();
for (Object handler : handlers) {
this.globalExceptionHandlers.put(handler, handlersCopy.get(handler));
}
}
/**
* Find an @{@link ExceptionHandler} method and invoke it to handle the
* raised exception.
@ -307,24 +370,32 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
* @return a method to handle the exception, or {@code null}
*/
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
if (handlerMethod == null) {
return null;
}
if (handlerMethod != null) {
Class<?> handlerType = handlerMethod.getBeanType();
Method method = getExceptionHandlerMethodResolver(handlerType).resolveMethod(exception);
return (method != null ? new ServletInvocableHandlerMethod(handlerMethod.getBean(), method) : null);
ExceptionHandlerMethodResolver resolver = this.exceptionHandlersByType.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlersByType.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
}
return getGlobalExceptionHandlerMethod(exception);
}
/**
* Return a method resolver for the given handler type, never {@code null}.
* Return a global {@code @ExceptionHandler} method for the given exception or {@code null}.
*/
private ExceptionHandlerMethodResolver getExceptionHandlerMethodResolver(Class<?> handlerType) {
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerMethodResolvers.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerMethodResolvers.put(handlerType, resolver);
private ServletInvocableHandlerMethod getGlobalExceptionHandlerMethod(Exception exception) {
for (Entry<Object, ExceptionHandlerMethodResolver> entry : this.globalExceptionHandlers.entrySet()) {
Method method = entry.getValue().resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey(), method);
}
return resolver;
}
return null;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -30,11 +30,17 @@ import java.util.Arrays;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
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.ExceptionResolver;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ModelMethodProcessor;
@ -63,10 +69,10 @@ public class ExceptionHandlerExceptionResolverTests {
@BeforeClass
public static void setupOnce() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.afterPropertiesSet();
RESOLVER_COUNT = resolver.getArgumentResolvers().getResolvers().size();
HANDLER_COUNT = resolver.getReturnValueHandlers().getHandlers().size();
ExceptionHandlerExceptionResolver r = new ExceptionHandlerExceptionResolver();
r.afterPropertiesSet();
RESOLVER_COUNT = r.getArgumentResolvers().getResolvers().size();
HANDLER_COUNT = r.getReturnValueHandlers().getHandlers().size();
}
@Before
@ -169,6 +175,40 @@ public class ExceptionHandlerExceptionResolverTests {
assertEquals("IllegalArgumentException", this.response.getContentAsString());
}
@Test
public void resolveExceptionGlobalHandler() throws UnsupportedEncodingException, NoSuchMethodException {
AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyConfig.class);
this.resolver.setApplicationContext(cxt);
this.resolver.setGlobalExceptionHandlers(new GlobalExceptionHandler());
this.resolver.afterPropertiesSet();
IllegalStateException ex = new IllegalStateException();
HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
assertNotNull("Exception was not handled", mav);
assertTrue(mav.isEmpty());
assertEquals("IllegalStateException", this.response.getContentAsString());
}
@Test
public void resolveExceptionGlobalHandlerOrdered() throws UnsupportedEncodingException, NoSuchMethodException {
AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyConfig.class);
this.resolver.setApplicationContext(cxt);
GlobalExceptionHandler globalHandler = new GlobalExceptionHandler();
globalHandler.setOrder(2);
this.resolver.setGlobalExceptionHandlers(globalHandler);
this.resolver.afterPropertiesSet();
IllegalStateException ex = new IllegalStateException();
HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle");
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
assertNotNull("Exception was not handled", mav);
assertTrue(mav.isEmpty());
assertEquals("@ExceptionResolver: IllegalStateException", this.response.getContentAsString());
}
private void assertMethodProcessorCount(int resolverCount, int handlerCount) {
assertEquals(resolverCount, this.resolver.getArgumentResolvers().getResolvers().size());
@ -204,7 +244,7 @@ public class ExceptionHandlerExceptionResolverTests {
@ExceptionHandler
@ResponseBody
public String handleException(Exception ex) {
public String handleException(IllegalArgumentException ex) {
return ClassUtils.getShortName(ex.getClass());
}
}
@ -219,4 +259,42 @@ public class ExceptionHandlerExceptionResolverTests {
}
}
static class GlobalExceptionHandler implements Ordered {
private int order;
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
@ExceptionHandler
@ResponseBody
public String handleException(IllegalStateException ex) {
return ClassUtils.getShortName(ex.getClass());
}
}
@ExceptionResolver
@Order(1)
static class AnnotatedExceptionResolver {
@ExceptionHandler
@ResponseBody
public String handleException(IllegalStateException ex) {
return "@ExceptionResolver: " + ClassUtils.getShortName(ex.getClass());
}
}
@Configuration
static class MyConfig {
@Bean public AnnotatedExceptionResolver exceptionResolver() {
return new AnnotatedExceptionResolver();
}
}
}

View File

@ -18,6 +18,8 @@ Changes in version 3.2 M2 (2012-08-xx)
* added support for the HTTP PATCH method to Spring MVC and to RestTemplate (SPR-7985)
* enable smart suffix pattern match in @RequestMapping methods (SPR-7632)
* DispatcherPortlet does not forward event exceptions to the render phase by default (SPR-9287)
* add defaultCharset property to StringHttpMessageConverter
* add @ExceptionResolver annotation to detect classes with @ExceptionHandler methods
Changes in version 3.2 M1 (2012-05-28)

View File

@ -3620,18 +3620,91 @@ public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData
<interfacename>HandlerExceptionResolver</interfacename> interface, which
is only a matter of implementing the
<literal>resolveException(Exception, Handler)</literal> method and
returning a <classname>ModelAndView</classname>, you may also use the
returning a <classname>ModelAndView</classname>, you may also use the provided
<classname>SimpleMappingExceptionResolver</classname>. This resolver
enables you to take the class name of any exception that might be thrown
and map it to a view name. This is functionally equivalent to the
exception mapping feature from the Servlet API, but it is also possible
to implement more finely grained mappings of exceptions from different
handlers.</para>
</section>
<section id="mvc-ann-exceptionhandler">
<title><interfacename>@ExceptionHandler</interfacename></title>
<para>The <interfacename>HandlerExceptionResolver</interfacename> interface
and the <classname>SimpleMappingExceptionResolver</classname> implementations
allow you to map Exceptions to specific views along with some Java logic
before forwarding to those views. However, in some cases, especially when
working with programmatic clients (Ajax or non-browser) it is more
convenient to set the status and optionally write error information to the
response body.</para>
<para>For that you can use <interfacename>@ExceptionHandler</interfacename>
methods. When present within a controller such methods apply to exceptions
raised by that contoroller or any of its sub-classes.
Or you can also declare <interfacename>@ExceptionHandler</interfacename>
methods in a type annotated with <interfacename>@ExceptionResolver</interfacename>
in which case they apply globally.
The <interfacename>@ExceptionResolver</interfacename> annotation is
a component annotation that can also be used with a component scan.
</para>
<para>Here is an example with a controller-level
<interfacename>@ExceptionHandler</interfacename> method:</para>
<programlisting language="java">@Controller
public class SimpleController {
// other controller method omitted
@ExceptionHandler(IOException.class)
public ResponseEntity handleIOException(IOException ex) {
// prepare responseEntity
return responseEntity;
}
}</programlisting>
<para>The <classname>@ExceptionHandler</classname> value can be set to
an array of Exception types. If an exception is thrown matches one of
the types in the list, then the method annotated with the matching
<classname>@ExceptionHandler</classname> will be invoked. If the
annotation value is not set then the exception types listed as method
arguments are used.</para>
<para>Much like standard controller methods annotated with a
<interfacename>@RequestMapping</interfacename> annotation, the method arguments
and return values of <interfacename>@ExceptionHandler</interfacename> methods
can be flexible. For example, the
<classname>HttpServletRequest</classname> can be accessed in Servlet
environments and the <classname>PortletRequest</classname> in Portlet
environments. The return type can be a <classname>String</classname>,
which is interpreted as a view name, a
<classname>ModelAndView</classname> object, a
<classname>ResponseEntity</classname>, or you can also add the
<interfacename>@ResponseBody</interfacename> to have the method return value
converted with message converters and written to the response stream.</para>
<note><para>To better understand how <interfacename>@ExceptionHandler</interfacename>
methods work, consider that in Spring MVC there is only one abstraction
for handling exception and that's the
<interfacename>HandlerExceptionResolver</interfacename>. There is a special
implementation of that interface,
the <classname>ExceptionHandlerExceptionResolver</classname>, which detects
and invokes <interfacename>@ExceptionHandler</interfacename> methods.</para></note>
</section>
<section id="mvc-ann-rest-handler-exception-resolvers">
<title><classname>DefaultHandlerExceptionResolver</classname>
and <classname>ResponseStatusExceptionResolver</classname></title>
<para>By default, the <classname>DispatcherServlet</classname> registers
the <classname>DefaultHandlerExceptionResolver</classname>. This
resolver handles certain standard Spring MVC exceptions by setting a
specific response status code: <informaltable>
specific response status code. This is useful when responding to programmatic
clients (e.g. Ajax, non-browser) in which the client uses the response code
to interpret the result.
<informaltable>
<tgroup cols="2">
<thead>
<row>
@ -3697,51 +3770,21 @@ public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData
</row>
</tbody>
</tgroup>
</informaltable></para>
</section>
</informaltable>
</para>
<section id="mvc-ann-exceptionhandler">
<title><interfacename>@ExceptionHandler</interfacename></title>
<para>The <classname>DispatcherServlet</classname> also registers the
<classname>ResponseStatusExceptionResolver</classname>, which handles
exceptions annotated with <interfacename>@ResponseStatus</interfacename>
by setting the response status code to that indicated in the annotation.
Once again this is useful in scenarios with programmatic clients.</para>
<para>An alternative to the
<interfacename>HandlerExceptionResolver</interfacename> interface is the
<interfacename>@ExceptionHandler</interfacename> annotation. You use the
<classname>@ExceptionHandler</classname> method annotation within a
controller to specify which method is invoked when an exception of a
specific type is thrown during the execution of controller methods. For
example:</para>
<programlisting language="java">@Controller
public class SimpleController {
// other controller method omitted
@ExceptionHandler(IOException.class)
public String handleIOException(IOException ex, HttpServletRequest request) {
return ClassUtils.getShortName(ex.getClass());
}
}</programlisting>
<para>will invoke the 'handlerIOException' method when a
<classname>java.io.IOException</classname> is thrown.</para>
<para>The <classname>@ExceptionHandler</classname> value can be set to
an array of Exception types. If an exception is thrown matches one of
the types in the list, then the method annotated with the matching
<classname>@ExceptionHandler</classname> will be invoked. If the
annotation value is not set then the exception types listed as method
arguments are used.</para>
<para>Much like standard controller methods annotated with a
<classname>@RequestMapping</classname> annotation, the method arguments
and return values of <classname>@ExceptionHandler</classname> methods
are very flexible. For example, the
<classname>HttpServletRequest</classname> can be accessed in Servlet
environments and the <classname>PortletRequest</classname> in Portlet
environments. The return type can be a <classname>String</classname>,
which is interpreted as a view name or a
<classname>ModelAndView</classname> object. Refer to the API
documentation for more details.</para>
<para>Note however that if you explicitly register one or more
<interfacename>HandlerExceptionResolver</interfacename> instances in your configuration
then the defaults registered by the <classname>DispatcherServlet</classname> are
cancelled. This is standard behavior with regards to
<classname>DispatcherServlet</classname> defaults.
See <xref linkend="mvc-servlet-special-bean-types"/> for more details.</para>
</section>
</section>