Annotation-based programming is convenient and popular as annotated controllers provide flexible method signatures and don’t have to extend base classed nor implement specific interfaces. In this blog, I plan to investigate one of those expressions - exception handling, which provides the ability to handle the global exception by annotations @ControllerAdvice
, @RestControllerAdvice
and @ExceptionHandler
.
Controller Advice
@ControllerAdvice
is meta-annotated with @Component
and therefore can be registered as a Spring bean through component scanning.
@RestControllerAdvice
is meta-annotated with @ControllerAdvice
and @ResponseBody
, and that means @ExceptionHandler
methods will have their return value rendered via response body message conversion, rather than via HTML views.
On startup, RequestMappingHandlerMapping
initiates ExceptionHandlerExceptionResolver
which will detect controller advice beans and apply them at runtime. Global @ExceptionHandler
methods, from an @ControllerAdvice
, are applied after local ones, from the @Controller
. By contrast, global @ModelAttribute
and @InitBinder
methods are applied before local ones.
private void initExceptionHandlerAdviceCache() {
if (this.getApplicationContext() != null) {
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
Iterator var2 = adviceBeans.iterator();
while(var2.hasNext()) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean)var2.next();
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (this.logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
this.logger.debug("ControllerAdvice beans: none");
} else {
this.logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
}
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)this.exceptionHandlerCache.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new);
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
}
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
Iterator var9 = this.exceptionHandlerAdviceCache.entrySet().iterator();
while(var9.hasNext()) {
Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry = (Map.Entry)var9.next();
ControllerAdviceBean advice = (ControllerAdviceBean)entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver)entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
The @ControllerAdvice
annotation has attributes that let you narrow the set of controllers and handlers that they apply to. For example:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
NOTE: The selectors in the preceding example are evaluated at runtime
and may negatively impact performance if used extensively.
Exceptions
@Controller
and @ControllerAdviceclasses
can have @ExceptionHandler
methods to handle exceptions from controller methods, as the following example shows:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
The exception may match against a top-level exception being propagated (e.g. a direct IOException
being thrown) or against a nested cause within a wrapper exception (e.g. an IOException
wrapped inside an IllegalStateException
).
For matching exception types, preferably declare the target exception as a method argument, as the preceding example shows. When multiple exception methods match, a root exception match is generally preferred to a cause exception match. More specifically, the ExceptionDepthComparator
is used to sort exceptions based on their depth from the thrown exception type.
please reference to class ExceptionHandlerMethodResolver
ExceptionDepthComparator
public class ExceptionHandlerMethodResolver {
// ....
// ....
@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
Method method = this.resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = this.resolveMethodByThrowable(cause);
}
}
return method;
}
@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = (Method)this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = this.getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, method);
}
return method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null;
}
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList();
Iterator var3 = this.mappedMethods.keySet().iterator();
while(var3.hasNext()) {
Class<? extends Throwable> mappedException = (Class)var3.next();
//Determines if the class or interface represented by this Class object is either the same as,
//or is a superclass or superinterface of, the class or interface represented by the specified Class parameter.
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
if (matches.size() > 1) {
matches.sort(new ExceptionDepthComparator(exceptionType));
}
return (Method)this.mappedMethods.get(matches.get(0));
} else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
}
}
// ....
// ....
}
public class ExceptionDepthComparator implements Comparator<Class<? extends Throwable>> {
private final Class<? extends Throwable> targetException;
// ...
// ...
public ExceptionDepthComparator(Class<? extends Throwable> exceptionType) {
Assert.notNull(exceptionType, "Target exception type must not be null");
this.targetException = exceptionType;
}
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
int depth1 = this.getDepth(o1, this.targetException, 0);
int depth2 = this.getDepth(o2, this.targetException, 0);
return depth1 - depth2;
}
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
if (exceptionToMatch.equals(declaredException)) {
return depth;
} else {
return exceptionToMatch == Throwable.class ? Integer.MAX_VALUE : this.getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}
}
// ...
// ...
}