Annotated Controllers - Exception handler

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);
            }
        }
    // ...
    // ...
}

Sequence diagrams

Client Filter Chain Servlet spring context controller Exception Handler Hello filter Hello Hello AOP Hello Exception Resolve exception & generate response Response Response Response Response Client Filter Chain Servlet spring context controller Exception Handler
  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值