Why repeat the wheel

Spring already comes with global exception interception, you might ask, so why reinvent the wheel?

That’s a good question, I think, for a couple of reasons

  1. Pack to force
  2. Spring’s global exception interception applies only to the Spring MVC interface, not to your RPC interface
  3. Unable to customize
  4. There are other things we can do besides write business code

I think the above reasons are sufficient to explain why we should repeat the wheel. Now let’s look at how to make the wheel

What kind of wheel?

I think global exception interception should have the following features

  1. Easy to use, preferably in the same way as Spring native use, reduce learning costs
  2. All interfaces are supported
  3. Exception handlers can be called predictably, such as the handler that defines RuntimeException and the handler that defines Exception. If NullPointException is thrown, the expected handler should be unambiguously selected

How do you make a wheel?

Since most applications today are spring-based, I also implement global exception interception based on Spring AOP

Start by defining a few annotations

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ExceptionAdvice {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    Class<? extends Throwable>[] value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionIntercept {
}
Copy the code

The purpose of @ExceptionAdvice is to flag the class that defines the exception handler so that it can be found easily

The purpose of @ExceptionHandler is to mark a method that handles exceptions and the value in it is the type of exception that can be handled

The purpose of @ExceptionIntercept is to flag methods that require exception interception

Next, define a uniform return format so that it returns uniformly when an error occurs

@Data
public class BaseResponse<T> {
    private Integer code;
    private String message;
    private T data;

    public BaseResponse(Integer code, String message) {
        this.code = code;
        this.message = message; }}Copy the code

Then define a class that collects exception handlers

public class ExceptionMethodPool {
    private List<ExceptionMethod> methods;
    private Object excutor;

    public ExceptionMethodPool(Object excutor) {
        this.methods = new ArrayList<ExceptionMethod>();
        this.excutor = excutor;
    }

    public Object getExcutor(a) {
        return excutor;
    }

    public void add(Class<? extends Throwable> clazz, Method method) {
        methods.add(new ExceptionMethod(clazz, method));
    }
		
  	// Find the handler that can handle the exception in order
    public Method obtainMethod(Throwable throwable) {
        return methods
                .stream()
                .filter(e -> e.getClazz().isAssignableFrom(throwable.getClass()))
                .findFirst()
                .orElseThrow(() ->new RuntimeException("No corresponding exception handler found"))
                .getMethod();
    }

    @AllArgsConstructor
    @Getter
    class ExceptionMethod {
        private Class<? extends Throwable> clazz;
        privateMethod method; }}Copy the code

There are two attributes inside ExceptionMethod

  • Clazz: This represents the exception that can be handled
  • Method: represents the method used to handle exception calls

ExceptionMethodPool holds all exception handlers in sequence, and Excutor is the object that executes those exception handlers

Next collect all defined exception handlers

@Component
public class ExceptionBeanPostProcessor implements BeanPostProcessor {
    private ExceptionMethodPool exceptionMethodPool;
    @Autowired
    private ConfigurableApplicationContext context;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Class<? > clazz = bean.getClass(); ExceptionAdvice advice = clazz.getAnnotation(ExceptionAdvice.class);if (advice == null) return bean;
        if(exceptionMethodPool ! =null) throw new RuntimeException("Two exception definition classes are not allowed.");
        exceptionMethodPool = new ExceptionMethodPool(bean);

        // Keep the exception method orderArrays.stream(clazz.getDeclaredMethods()) .filter(method -> method.getAnnotation(ExceptionHandler.class) ! =null)
                .forEach(method -> {
                    ExceptionHandler exceptionHandler = method.getAnnotation(ExceptionHandler.class);
                    Arrays.stream(exceptionHandler.value()).forEach(c -> exceptionMethodPool.add(c,method));
                });
        // Register with the Spring container
        context.getBeanFactory().registerSingleton("exceptionMethodPool",exceptionMethodPool);
        returnbean; }}Copy the code

ExceptionBeanPostProcessor by implementing the BeanPostProcessor interface, before the bean initialization, put all the exception handler into ExceptionMethodPool, and its registered into the Spring container

Then define the exception handler

@Component
// Make ExceptionProcessor load after ExceptionConfig to ensure exceptionPool has a value
@DependsOn("exceptionConfig")
public class ExceptionProcessor {
    @Autowired
    private ExceptionMethodPool exceptionMethodPool;

    public BaseResponse process(Throwable e) {
        return (BaseResponse) FunctionUtil.computeOrGetDefault(() ->{
            Method method = exceptionMethodPool.obtainMethod(e);
            method.setAccessible(true);
            return method.invoke(exceptionMethodPool.getExcutor(),e);
        },new BaseResponse(0."Unknown error")); }}Copy the code

This applies some of my own syntax sugar encapsulated by functional programming, if you’re interested

Finally, AOP is used for interception

@Aspect
@Component
public class ExceptionInterceptAop {
    @Autowired
    private ExceptionProcessor exceptionProcessor;
  
    @Pointcut("@annotation(com.example.exception.intercept.ExceptionIntercept)")
    public void pointcut(a) {}@Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {
        return computeAndDealException(() -> point.proceed(),
                e -> exceptionProcessor.process(e));
    }
  
    public static <R> R computeAndDealException(ThrowExceptionSupplier<R> supplier, Function<Throwable, R> dealFunc) {
        try {
            return supplier.get();
        } catch (Throwable e) {
            returndealFunc.apply(e); }}@FunctionalInterface
    public interface ThrowExceptionSupplier<T> {
        T get(a) throws Throwable; }}Copy the code

Now that we’ve done the code, let’s see how we can use it. Okay

@ExceptionAdvice
public class ExceptionConfig {
    @ExceptionHandler(value = NullPointerException.class)
    public BaseResponse process(NullPointerException e){
        return new BaseResponse(0."NPE");
    }

    @ExceptionHandler(value = Exception.class)
    public BaseResponse process(Exception e){
        return new BaseResponse(0."Ex"); }}@RestController
public class TestControler {

    @RequestMapping("/test")
    @ExceptionIntercept
    public BaseResponse test(@RequestParam("a") Integer a){
        if (a == 1) {return new BaseResponse(1,a+"");
        }
        else if (a == 2) {throw new NullPointerException();
        }
        else throw newRuntimeException(); }}Copy the code

We use the @exceptionadvice flag to define the ExceptionHandler class, and then use the @exceptionhandler annotation to handle exceptions for easy collection

Finally, exception interception is performed on the method requiring exception interception by @ExceptionIntercept

Instead of using Spring’s approach of matching the nearest parent to find a matching exception handler, I find this design a failure for several reasons

  • Complex code
  • You can’t see at a glance which exception handler to call, especially if there are a lot of exception handlers defined, and it’s even harder to find multiple definition classes. You may have to look at all the handlers before you know which one to call

For that reason, I kept only one exception handler definition class and matched it in the same order as the method definition, from top to bottom, so that as long as I found a handler that could handle it, I would know how to call it. Okay

Add the Github address

Thank you very much @ you go ahead, ok? Hide bug analysis portal

Original is not easy, if you feel helpful, trouble to praise!

I will share some interesting techniques from time to time, click follow not to get lost -. –