在spring中手写全局异常拦 截器

作者:CrazyMark 时间:2023-11-09 01:25:42 

为什么要重复造轮子

你可能会问,Spring已经自带了全局异常拦截,为什么还要重复造轮子呢?

这是个好问题,我觉得有以下几个原因

  1. 装逼

  2. Spring的全局异常拦截只是针对于Spring MVC的接口,对于你的RPC接口就无能为力了

  3. 无法定制化

  4. 除了写业务代码,我们其实还能干点别的事

我觉得上述理由已经比较充分的解答了为什么要重复造轮子,接下来就来看一下怎么造轮子

造个什么样的轮子?

我觉得全局异常拦截应该有如下特性

  1. 使用方便,最好和spring原生的使用方式一致,降低学习成本

  2. 能够支持所有接口

  3. 调用异常处理器可预期,比如说定义了RuntimeException的处理器和Exception的处理器,如果这个时候抛出NullPointException,这时候要能没有歧义的选择预期的处理器

如何造轮子?

由于现在的应用基本上都是基于spring的,因此我也是基于SpringAop来实现全局异常拦截

首先先定义几个注解


@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 {
}

@ExceptionAdvice 的作用是标志定义异常处理器的类,方便找到异常处理器

@ExceptionHandler 的作用是标记某个方法是处理异常的,里面的值是能够处理的异常类型

@ExceptionIntercept 的作用是标记需要异常拦截的方法

接下来定义统一返回格式,以便出现错误的时候统一返回


@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;
 }
}

然后定义一个收集异常处理器的类


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() {
   return excutor;
 }
public void add(Class<? extends Throwable> clazz, Method method) {
   methods.add(new ExceptionMethod(clazz, method));
 }
//按序查找能够处理该异常的处理器
 public Method obtainMethod(Throwable throwable) {
   return methods
       .stream()
       .filter(e -> e.getClazz().isAssignableFrom(throwable.getClass()))
       .findFirst()
       .orElseThrow(() ->new RuntimeException("没有找到对应的异常处理器"))
       .getMethod();
 }
@AllArgsConstructor
 @Getter
 class ExceptionMethod {
   private Class<? extends Throwable> clazz;
   private Method method;
 }
}

ExceptionMethod 里面有两个属性

  • clazz:这个代表着能够处理的异常

  • method:代表着处理异常调用的方法

ExceptionMethodPool 里面按序存放所有异常处理器,excutor是执行这些异常处理器的对象

接下来把所有定义的异常处理器收集起来


@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("不允许有两个异常定义类");
   exceptionMethodPool = new ExceptionMethodPool(bean);
//保持处理异常方法顺序
   Arrays.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));
       });
   //注册进spring容器
   context.getBeanFactory().registerSingleton("exceptionMethodPool",exceptionMethodPool);
   return bean;
 }
}

ExceptionBeanPostProcessor 通过实现BeanPostProcessor 接口,在bean初始化之前,把所有异常处理器塞进 ExceptionMethodPool,并把其注册进Spring容器

然后定义异常处理器


@Component
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,"未知错误"));
 }
}

这里应用了我自己通过函数式编程封装的一些语法糖,有兴趣的可以看下

最后通过AOP进行拦截


@Aspect
@Component
public class ExceptionInterceptAop {
 @Autowired
 private ExceptionProcessor exceptionProcessor;
@Pointcut("@annotation(com.example.exception.intercept.ExceptionIntercept)")
 public void pointcut() {
 }
@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) {
     return dealFunc.apply(e);
   }
 }
 @FunctionalInterface
 public interface ThrowExceptionSupplier<T> {
   T get() throws Throwable;
 }
}

到这里代码部分就已经完成了,我们来看下如何使用


@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 new RuntimeException();
 }
}

我们通过@ExceptionAdvice标志定义异常处理器的类,然后通过@ExceptionHandler标注处理异常的方法,方便收集

最后在需要异常拦截的方法上面通过@ExceptionIntercept进行异常拦截

我没有使用Spring那种匹配最近父类的方式寻找匹配的异常处理器,我觉得这种设计是一个败笔,理由如下

  • 代码复杂

  • 不能一眼看出要去调用哪个异常处理器,尤其是定义的异常处理器非常多的时候,要是弄多个定义类就更不好找了,可能要把所有的处理器看完才知道应该调用哪个

出于以上考虑,我只保留了一个异常处理器定义类,并且匹配顺序和方法定义顺序一致,从上到下依次匹配,这样只要找到一个能够处理的处理器,那么就知道了会如何调用

原创不易,如果觉得对你有帮助,麻烦点个赞!

我会不定期分享一些有意思的技术,点个关注不迷路-。 -

来源:https://juejin.im/post/6895541052581609486?utm_source=tuicool&utm_medium=referral

标签:spring,异常,拦截
0
投稿

猜你喜欢

  • mybatis Interceptor对UpdateTime自动处理的实现方法

    2023-10-13 16:02:20
  • 详解Android开发中Activity的四种launchMode

    2023-05-19 08:30:27
  • 浅谈Mybatis传参类型如何确定

    2023-11-12 12:13:21
  • java 打造阻塞式线程池的实例详解

    2022-09-13 09:50:33
  • 详解C#实现在Excel单元格中应用多种字体格式

    2023-01-25 07:58:52
  • C#实现简单的天气预报示例代码

    2022-03-22 22:52:59
  • @Async导致controller 404及失效原因解决分析

    2021-12-17 01:51:44
  • android如何获取联系人所有信息

    2021-10-24 13:24:41
  • Spring Cloud 使用 Resilience4j 实现服务熔断的方法

    2021-11-09 12:36:10
  • Java多线程实现Callable接口

    2022-09-01 17:53:54
  • Java KeyGenerator.generateKey的19个方法代码示例

    2022-08-08 14:26:40
  • java编译器和JVM的区别

    2023-07-18 20:34:48
  • SpringBoot使用validation-api实现对枚举类参数校验的方法

    2021-09-22 19:21:32
  • c#初学简单程序实例代码介绍

    2023-11-04 10:53:53
  • 代理角色java设计模式之静态代理详细介绍

    2022-10-14 16:48:03
  • springboot自定义Starter的具体流程

    2022-01-26 05:08:06
  • SpringBoot整合ES-Elasticsearch的实例

    2022-12-29 03:30:40
  • Java实现替换PDF中的字体功能

    2023-10-04 13:24:17
  • 使用Springboot搭建OAuth2.0 Server的方法示例

    2023-01-28 07:19:16
  • Java中List.of()和Arrays.asList()的区别及原因分析

    2023-08-06 00:33:16
  • asp之家 软件编程 m.aspxhome.com