Skip to content

Commit c846198

Browse files
committed
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
1 parent ccd2da3 commit c846198

File tree

6 files changed

+335
-75
lines changed

6 files changed

+335
-75
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@
7676
* {@link org.springframework.web.servlet.RequestToViewNameTranslator}.
7777
* <li>A {@link org.springframework.web.servlet.View} object.
7878
* <li>A {@link java.lang.String} value which is interpreted as view name.
79+
* <li>{@link ResponseBody @ResponseBody} annotated methods (Servlet-only)
80+
* to set the response content. The return value will be converted to the
81+
* response stream using
82+
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
83+
* <li>An {@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
84+
* {@link org.springframework.http.ResponseEntity ResponseEntity&lt;?&gt;} object
85+
* (Servlet-only) to set response headers and content. The ResponseEntity body
86+
* will be converted and written to the response stream using
87+
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
7988
* <li><code>void</code> if the method handles the response itself (by
8089
* writing the response content directly, declaring an argument of type
8190
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.bind.annotation;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.core.Ordered;
26+
import org.springframework.core.annotation.Order;
27+
import org.springframework.stereotype.Component;
28+
29+
/**
30+
* An {@linkplain Component @Component} annotation that indicates the annotated class
31+
* contains {@linkplain ExceptionHandler @ExceptionHandler} methods. Such methods
32+
* will be used in addition to {@code @ExceptionHandler} methods in
33+
* {@code @Controller}-annotated classes.
34+
*
35+
* <p>In order for the the annotation to detected, an instance of
36+
* {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}
37+
* is configured.
38+
*
39+
* <p>Classes with this annotation may use the {@linkplain Order @Order} annotation
40+
* or implement the {@link Ordered} interface to indicate the order in which they
41+
* should be used relative to other such annotated components. However, note that
42+
* the order is only for components registered through {@code @ExceptionResolver},
43+
* i.e. within an
44+
* {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}.
45+
*
46+
* @author Rossen Stoyanchev
47+
* @since 3.2
48+
*
49+
* @see org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
50+
*/
51+
@Target(ElementType.TYPE)
52+
@Retention(RetentionPolicy.RUNTIME)
53+
@Documented
54+
@Component
55+
public @interface ExceptionResolver {
56+
57+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,31 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
2123
import java.util.List;
2224
import java.util.Map;
25+
import java.util.Map.Entry;
2326
import java.util.concurrent.ConcurrentHashMap;
2427

2528
import javax.servlet.http.HttpServletRequest;
2629
import javax.servlet.http.HttpServletResponse;
2730
import javax.xml.transform.Source;
2831

32+
import org.springframework.beans.BeansException;
2933
import org.springframework.beans.factory.InitializingBean;
34+
import org.springframework.context.ApplicationContext;
35+
import org.springframework.context.ApplicationContextAware;
36+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
37+
import org.springframework.core.annotation.AnnotationUtils;
3038
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
3139
import org.springframework.http.converter.HttpMessageConverter;
3240
import org.springframework.http.converter.StringHttpMessageConverter;
3341
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
3442
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
3543
import org.springframework.web.accept.ContentNegotiationManager;
3644
import org.springframework.web.bind.annotation.ExceptionHandler;
45+
import org.springframework.web.bind.annotation.ExceptionResolver;
3746
import org.springframework.web.context.request.ServletWebRequest;
3847
import org.springframework.web.method.HandlerMethod;
3948
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
@@ -49,6 +58,8 @@
4958
import org.springframework.web.servlet.View;
5059
import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
5160

61+
import edu.emory.mathcs.backport.java.util.Collections;
62+
5263
/**
5364
* An {@link AbstractHandlerMethodExceptionResolver} that resolves exceptions
5465
* through {@code @ExceptionHandler} methods.
@@ -62,7 +73,7 @@
6273
* @since 3.1
6374
*/
6475
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements
65-
InitializingBean {
76+
InitializingBean, ApplicationContextAware {
6677

6778
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
6879

@@ -72,13 +83,18 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
7283

7384
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
7485

75-
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers =
76-
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
86+
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlersByType =
87+
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>();
88+
89+
private final Map<Object, ExceptionHandlerMethodResolver> globalExceptionHandlers =
90+
new LinkedHashMap<Object, ExceptionHandlerMethodResolver>();
7791

7892
private HandlerMethodArgumentResolverComposite argumentResolvers;
7993

8094
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
8195

96+
private ApplicationContext applicationContext;
97+
8298
/**
8399
* Default constructor.
84100
*/
@@ -193,6 +209,22 @@ public void setContentNegotiationManager(ContentNegotiationManager contentNegoti
193209
this.contentNegotiationManager = contentNegotiationManager;
194210
}
195211

212+
/**
213+
* Provide instances of objects with {@link ExceptionHandler @ExceptionHandler}
214+
* methods to apply globally, i.e. regardless of the selected controller.
215+
* <p>{@code @ExceptionHandler} methods in the controller are always looked
216+
* up before {@code @ExceptionHandler} methods in global handlers.
217+
*/
218+
public void setGlobalExceptionHandlers(Object... handlers) {
219+
for (Object handler : handlers) {
220+
this.globalExceptionHandlers.put(handler, new ExceptionHandlerMethodResolver(handler.getClass()));
221+
}
222+
}
223+
224+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
225+
this.applicationContext = applicationContext;
226+
}
227+
196228
public void afterPropertiesSet() {
197229
if (this.argumentResolvers == null) {
198230
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
@@ -202,6 +234,7 @@ public void afterPropertiesSet() {
202234
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
203235
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
204236
}
237+
initGlobalExceptionHandlers();
205238
}
206239

207240
/**
@@ -255,6 +288,36 @@ protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers()
255288
return handlers;
256289
}
257290

291+
private void initGlobalExceptionHandlers() {
292+
if (this.applicationContext == null) {
293+
logger.warn("Can't detect @ExceptionResolver components if the ApplicationContext property is not set");
294+
}
295+
else {
296+
String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);
297+
for (String name : beanNames) {
298+
Class<?> type = this.applicationContext.getType(name);
299+
if (AnnotationUtils.findAnnotation(type , ExceptionResolver.class) != null) {
300+
Object bean = this.applicationContext.getBean(name);
301+
this.globalExceptionHandlers.put(bean, new ExceptionHandlerMethodResolver(bean.getClass()));
302+
}
303+
}
304+
}
305+
if (this.globalExceptionHandlers.size() > 0) {
306+
sortGlobalExceptionHandlers();
307+
}
308+
}
309+
310+
private void sortGlobalExceptionHandlers() {
311+
Map<Object, ExceptionHandlerMethodResolver> handlersCopy =
312+
new HashMap<Object, ExceptionHandlerMethodResolver>(this.globalExceptionHandlers);
313+
List<Object> handlers = new ArrayList<Object>(handlersCopy.keySet());
314+
Collections.sort(handlers, new AnnotationAwareOrderComparator());
315+
this.globalExceptionHandlers.clear();
316+
for (Object handler : handlers) {
317+
this.globalExceptionHandlers.put(handler, handlersCopy.get(handler));
318+
}
319+
}
320+
258321
/**
259322
* Find an @{@link ExceptionHandler} method and invoke it to handle the
260323
* raised exception.
@@ -307,24 +370,32 @@ protected ModelAndView doResolveHandlerMethodException(HttpServletRequest reques
307370
* @return a method to handle the exception, or {@code null}
308371
*/
309372
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
310-
if (handlerMethod == null) {
311-
return null;
373+
if (handlerMethod != null) {
374+
Class<?> handlerType = handlerMethod.getBeanType();
375+
ExceptionHandlerMethodResolver resolver = this.exceptionHandlersByType.get(handlerType);
376+
if (resolver == null) {
377+
resolver = new ExceptionHandlerMethodResolver(handlerType);
378+
this.exceptionHandlersByType.put(handlerType, resolver);
379+
}
380+
Method method = resolver.resolveMethod(exception);
381+
if (method != null) {
382+
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
383+
}
312384
}
313-
Class<?> handlerType = handlerMethod.getBeanType();
314-
Method method = getExceptionHandlerMethodResolver(handlerType).resolveMethod(exception);
315-
return (method != null ? new ServletInvocableHandlerMethod(handlerMethod.getBean(), method) : null);
385+
return getGlobalExceptionHandlerMethod(exception);
316386
}
317387

318388
/**
319-
* Return a method resolver for the given handler type, never {@code null}.
389+
* Return a global {@code @ExceptionHandler} method for the given exception or {@code null}.
320390
*/
321-
private ExceptionHandlerMethodResolver getExceptionHandlerMethodResolver(Class<?> handlerType) {
322-
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerMethodResolvers.get(handlerType);
323-
if (resolver == null) {
324-
resolver = new ExceptionHandlerMethodResolver(handlerType);
325-
this.exceptionHandlerMethodResolvers.put(handlerType, resolver);
391+
private ServletInvocableHandlerMethod getGlobalExceptionHandlerMethod(Exception exception) {
392+
for (Entry<Object, ExceptionHandlerMethodResolver> entry : this.globalExceptionHandlers.entrySet()) {
393+
Method method = entry.getValue().resolveMethod(exception);
394+
if (method != null) {
395+
return new ServletInvocableHandlerMethod(entry.getKey(), method);
396+
}
326397
}
327-
return resolver;
398+
return null;
328399
}
329400

330401
}

0 commit comments

Comments
 (0)