使用spring的AOP进行统一异常的处理

1. 使用切面管理异常的原因:

  1. 使用大量的try/catch来捕获异常
  2. 导致整个控制层代码可读性极差,并且此类工作重复枯燥、容易复制错。
  3. 后端的错误直接抛到前端页面用户体验不好

2. 解决方案springboot引入AOP

在SpringBoot中引入AOP是一件很方便的事,和其他引入依赖一样,我们只需要在POM中引入starter就可以了:

<!--spring切面aop依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.1返回体报文定义

接下来我们先想一下,一般我们返回体是什么样子的呢?或者你觉得一个返回的报文应该具有哪些特征。

成功标示:可以用boolean型作为标示位。

错误代码:一般用整型作为标示位,罗列的越详细,前端的容错也就能做的更细致。

错误信息:使用String作为错误信息的描述,留给前端是否展示给用户或者进入其他错误流程的使用。

结果集:在无错误信息的情况下所得到的正确数据信息。一般是个Map,前端根据Key取值。

以上是对一个返回体报文一个粗略的定义了,如果再细致点,可以使用签名进行验签功能活着对明文数据进行对称加密等等。这些我们今天先不讨论,我们先完成一个能够使用的接口信息定义。

我们再对以上提到这些信息做一个完善,去除冗余的字段,对差不多的类型进行合并于封装。这样的想法下,我们创建一个返回体报文的实体类。

public class Result<T> {
 
   //    error_code 状态值:0 极为成功,其他数值代表失败
   private Integer status;
 
   //    error_msg 错误信息,若status为0时,为success
   private String msg;
 
   //    content 返回体报文的出参,使用泛型兼容不同的类型
   private T data;
 
   public Integer getStatus() {
       return status;
   }
 
   public void setStatus(Integer code) {
       this.status = code;
   }
 
   public String getMsg() {
       return msg;
   }
 
   public void setMsg(String msg) {
       this.msg = msg;
   }
 
   public T getData(Object object) {
       return data;
   }
 
   public void setData(T data) {
       this.data = data;
   }
 
   public T getData() {
       return data;
   }
 
   @Override
   public String toString() {
       return "Result{" +
               "status=" + status +
               ", msg='" + msg + '\'' +
               ", data=" + data +
               '}';
   }


2.2 常见已知错误的枚举类

现在我们已经有一个返回体报文的定义了,那接下来我们可以来创建一个枚举类,来记录一些我们已知的错误信息,可以在代码中直接使用。

public enum ExceptionEnum {
    UNKNOW_ERROR(-1,"未知错误"),
    USER_NOT_FIND(-101,"用户不存在"),
;
 
    private Integer code;
 
    private String msg;
 
    ExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
 
    public Integer getCode() {
        return code;
    }
 
    public String getMsg() {
        return msg;
    }
}

我们在这里把对于不再预期内的错误统一设置为-1,未知错误。以避免返回给前端大段大段的错误信息。

2.3 创建返回结果的工具类

接下来我们只需要创建一个工具类在代码中使用:

public class ResultUtil {
 
    /**
     * 返回成功,传入返回体具体出參
     * @param object
     * @return
     */
    public static Result success(Object object){
        Result result = new Result();
        result.setStatus(0);
        result.setMsg("success");
        result.setData(object);
        return result;
    }
 
    /**
     * 提供给部分不需要出參的接口
     * @return
     */
    public static Result success(){
        return success(null);
    }
 
    /**
     * 自定义错误信息
     * @param code
     * @param msg
     * @return
     */
    public static Result error(Integer code,String msg){
        Result result = new Result();
        result.setStatus(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
 
    /**
     * 返回异常信息,在已知的范围内
     * @param exceptionEnum
     * @return
     */
    public static Result error(ExceptionEnum exceptionEnum){
        Result result = new Result();
        result.setStatus(exceptionEnum.getCode());
        result.setMsg(exceptionEnum.getMsg());
        result.setData(null);
        return result;
    }
}

以上我们已经可以捕获代码中那些在编码阶段我们已知的错误了,但是却无法捕获程序出的未知异常信息。我们的代码应该写得漂亮一点,虽然很多时候我们会说时间太紧了,等之后我再来好好优化。可事实是,我们再也不会回来看这些代码了。项目总是一个接着一个,时间总是不够用的。如果真的需要你完善重构原来的代码,那你一定会非常痛苦,死得相当难看。所以,在第一次构建时,就将你的代码写完善了。

3.4 自定义异常

一般系统抛出的错误是不含错误代码的,除去部分的404,400,500错误之外,我们如果想把错误代码定义的更细致,就需要自己继承RuntimeException这个类后重新定义一个构造方法来定义我们自己的错误信息:

public class DescribeException extends RuntimeException{
 
    private Integer code;
 
    /**
     * 继承exception,加入错误状态值
     * @param exceptionEnum
     */
    public DescribeException(ExceptionEnum exceptionEnum) {
        super(exceptionEnum.getMsg());
        this.code = exceptionEnum.getCode();
    }
 
    /**
     * 自定义错误信息
     * @param message
     * @param code
     */
    public DescribeException(String message, Integer code) {
        super(message);
        this.code = code;
    }
 
    public Integer getCode() {
        return code;
    }
 
    public void setCode(Integer code) {
        this.code = code;
    }
}

3.5定义处理器记录未知异常

同时,我们使用一个Handle来把Try,Catch中捕获的错误进行判定,是一个我们已知的错误信息,还是一个未知的错误信息,如果是未知的错误信息,那我们就用log记录它,便于之后的查找和解决:

    @ControllerAdvice
    public class ExceptionHandle {
 
      private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
 
      /**
       * 判断错误是否是已定义的已知错误,不是则由未知错误代替,同时记录在log中
       * @param e
       * @return
       */
      @ExceptionHandler(value = Exception.class)
      @ResponseBody
      public Result exceptionGet(Exception e){
          if(e instanceof DescribeException){
              DescribeException MyException = (DescribeException) e;
              return ResultUtil.error(MyException.getCode(),MyException.getMessage());
          }
 
          LOGGER.error("【系统异常】{}",e);
          return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR);
      }
    }

这里我们使用了 @ControllerAdvice ,使Spring能加载该类,同时我们将所有捕获的异常统一返回结果Result这个实体。

此时,我们已经完成了对结果以及异常的统一返回管理,并且在出现异常时,我们可以不返回错误信息给前端,而是用未知错误进行代替,只有查看log我们才会知道真实的错误信息。

可能有小伙伴要问了,说了这么久,并没有使用到AOP啊。不要着急,我们继续完成我们剩余的工作。

3.6 定义一个切面类

我们使用接口若出现了异常,很难知道是谁调用接口,是前端还是后端出现的问题导致异常的出现,那这时,AOP久发挥作用了,我们之前已经引入了AOP的依赖,现在我们编写一个切面类,切点如何配置不需要我多说了吧:

@Aspect
@Component
public class HttpAspect {
 
    private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class);
 
    @Autowired
    private ExceptionHandle exceptionHandle;
 
    @Pointcut("execution(public * com.zzp.controller.*.*(..))")
    public void log(){
 
    }
 
    @Before("log()")
    public void doBefore(JoinPoint joinPoint){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
 
        //url
        LOGGER.info("url={}",request.getRequestURL());
        //method
        LOGGER.info("method={}",request.getMethod());
        //ip
        LOGGER.info("id={}",request.getRemoteAddr());
        //class_method
        LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName());
        //args[]
        LOGGER.info("args={}",joinPoint.getArgs());
    }
 
    @Around("log()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Result result = null;
        try {
 
        } catch (Exception e) {
            return exceptionHandle.exceptionGet(e);
        }
        if(result == null){
            return proceedingJoinPoint.proceed();
        }else {
            return result;
        }
    }
 
    @AfterReturning(pointcut = "log()",returning = "object")//打印输出结果
    public void doAfterReturing(Object object){
        LOGGER.info("response={}",object.toString());
    }
}

我们使用@Aspect来声明这是一个切面,使用@Pointcut来定义切面所需要切入的位置,这里我们是对每一个HTTP请求都需要切入,在进入方法之前我们使用@Before记录了调用的接口URL,调用的方法,调用方的IP地址以及输入的参数等。在整个接口代码运作期间,我们使用@Around来捕获异常信息,并用之前定义好的Result进行异常的返回,最后我们使用@AfterReturning来记录我们的出參。

3.7测试

以上全部,我们就完成了异常的统一管理以及切面获取接口信息,接下来我们心新写一个ResultController来测试一下:

@RestController
@RequestMapping("/result")
public class ResultController {
 
    @Autowired
    private ExceptionHandle exceptionHandle;
 
    /**
     * 返回体测试
     * @param name
     * @param pwd
     * @return
     */
    @RequestMapping(value = "/getResult",method = RequestMethod.POST)
    public Result getResult(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
        Result result = ResultUtil.success();
        try {
            if (name.equals("zzp")){
                result =  ResultUtil.success(new UserInfo());
            }else if (name.equals("pzz")){
                result =  ResultUtil.error(ExceptionEnum.USER_NOT_FIND);
            }else{
                int i = 1/0;
            }
        }catch (Exception e){
            result =  exceptionHandle.exceptionGet(e);
        }
        return result;
    }
}

在上面我们设计了一个controller,如果传入的name是zzp的话,我们就返回一个用户实体类,如果传入的是pzz的话,我们返回一个没有该用户的错误,其他的,我们让他抛出一个by zero的异常。
我们用POSTMAN进行下测试:
在这里插入图片描述
在这里插入图片描述
我们可以看到,前端收到的返回体报文已经按我们要求同意了格式,并且在控制台中我们打印出了调用该接口的一些接口信息,我们继续测试另外两个会出现错误情况的请求:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以看到,如是我们之前在代码中定义完成的错误信息,我们可以直接返回错误码以及错误信息,如果是程序出现了我们在编码阶段不曾预想到的错误,则统一返回未知错误,并在log中记录真实错误信息。

项目源码地址:

https://github.com/Dr-Water/global-exception

SpringAOP 全称为 Spring Aspect-Oriented Programming,是 Spring 框架中的一个模块,用于实现面向切面编程。在 SpringAOP 中,我们可以通过定义切点和通知来实现对目标对象的增强。 在平台开发中,我们可以使用 SpringAOP 来解决平台统一异常处理的问题。具体实现步骤如下: 1. 定义异常处理切面 在 SpringAOP 中,我们可以使用 @Aspect 注解来定义切面。在切面类中,我们需要定义一个处理异常的方法,并使用 @AfterThrowing 注解来指定这个方法是用于处理异常的。例如: ```java @Aspect public class ExceptionHandlerAspect { @AfterThrowing(pointcut = "execution(* com.example.*.*(..))", throwing = "ex") public void handleException(Exception ex) { // 处理异常的逻辑 } } ``` 在上述代码中,我们定义了一个名为 ExceptionHandlerAspect 的切面类,并在其中定义了一个名为 handleException 的方法,该方法的参数为 Exception 类型,用于处理异常。我们使用 @AfterThrowing 注解来指定这个方法是用于处理异常的,pointcut 属性用于指定切入点,这里我们使用 execution 表达式来匹配 com.example 包及其子包下的所有方法。 2. 配置 SpringAOPSpring 配置文件中,我们需要配置 SpringAOP。具体来说,我们需要定义一个名为 exceptionHandlerAspect 的 bean,并在其中指定切面的实现类。例如: ```xml <bean id="exceptionHandlerAspect" class="com.example.ExceptionHandlerAspect"/> <aop:config> <aop:aspect ref="exceptionHandlerAspect"> <aop:after-throwing method="handleException" throwing="ex"/> </aop:aspect> </aop:config> ``` 在上述代码中,我们首先定义了一个名为 exceptionHandlerAspect 的 bean,该 bean 的 class 属性指定了切面的实现类。然后,在 aop:config 标签中,我们使用 aop:aspect 标签来定义切面,并在其中指定切面的实现类。在 aop:aspect 标签中,我们使用 aop:after-throwing 标签来指定切面所织入的方法,method 属性用于指定处理异常的方法名,throwing 属性用于指定方法抛出的异常类型。 3. 使用统一异常处理 在业务代码中,当发生异常时,SpringAOP 会自动调用我们定义的异常处理方法。在这个方法中,我们可以根据具体的业务需求来处理异常。例如: ```java @Service public class UserServiceImpl implements UserService { @Override public void addUser(User user) { // 业务逻辑 try { // 执行业务操作 } catch (Exception e) { throw new RuntimeException("添加用户失败!"); } } } ``` 在上述代码中,当执行 addUser 方法时,如果发生异常SpringAOP 会自动调用我们定义的异常处理方法 handleException,并将异常对象作为参数传入该方法中。在 handleException 方法中,我们可以根据具体的业务需求来处理异常,例如记录日志、发送邮件等。 通过使用 SpringAOP,我们可以实现平台统一异常处理,避免代码中出现大量的 try-catch 代码块,提高代码的可读性和可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值