浅学JAVA开发中异常处理

优雅处理异常

异常处理逻辑上的分类:

对于我们并不期望会发生的事,我们可以使用异常捕捉;

对于我们觉得可能会发生的事,使用返回码。

springboot异常处理

  1. 自定义错误页面

    1. SpringBoot 默认的处理异常的机制:SpringBoot 默认的已经提供了一套处理异常的机制。一旦程序中出现了异常 SpringBoot 会向/error 的 url 发送请求。
    2. 在 springBoot 中提供了一个叫 BasicExceptionController 来处理/error 请求,然后跳转到默认显示异常的页面来展示异常信息。
    3. 我们也可以重写、error方法,进行特定的返回处理。eg:/error/500,/error/404;
    4. 场景:不是前后端分离,返回页面;
  2. 自己在controller层对指定异常进行try/catch捕获

    1. ```java
      @RequestMapping(“doCompute/{n1}/{n2}”)

      @ResponseBody
      public String doCompute(@PathVariable  Integer n1,
                              @PathVariable Integer n2){
          try{
              Integer result=n1/n2;
              return "Result is "+result;
          }catch(ArithmeticException e){
              return "exception is "+e.getMessage();
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      一个Controller类中通常会有多个方法,这样多个方法中都写try语句进行异常处理会带来大量重复代码的编写,不易维护。

      使用场景:特定的返回结果;

      3. #### @ExceptionHandle 注解处理异常

      1. Controller内部定义异常处理方法

      ```java
      @ExceptionHandler(ArithmeticException.class)
      @ResponseBody
      public String doHandleArithmeticException(ArithmeticException e){
      e.printStackTrace();
      return "计算过程中出现了异常,异常信息为"+e.getMessage();
      }

      @ExceptionHandler注解描述的方法为异常处理方法(注解中的异常类型为可处理的异常类型),假如Controller类中的逻辑方法中出现异常后没有处理异常,则会查找Controller类中有没有定义异常处理方法,假如定义了,且可以处理抛出的异常类型,则由异常处理方法处理异常。

    2. @ControllerAdvice+@ExceptionHandler 全局处理异常

      1. ```java
        @RestControllerAdvice
        @Slf4j
        public class ExceptionAdvice {
        @ExceptionHandler(ArithmeticException.class)
        public String doHandleArithmeticException(ArithmeticException e){
            e.printStackTrace();
            return  "计算过程中出现了异常,异常信息为"+e.getMessage();
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17

        2. @RestControllerAdvice 注解描述的类为全局异常处理类,当控制层方法中的异常没有自己捕获,也没有定义其内部的异常处理方法,底层默认会查找全局异常处理类,调用对应的异常处理方法进行异常处理。

        4. #### 自定义的异常类以及枚举类(优化)

        上述的示例中,我们对捕获的异常进行简单的二次处理,返回异常的信息,虽然这种能够让我们知道异常的原因,但是在很多的情况下来说,可能还是不够人性化,不符合我们的要求。

        1. 定义一个错误码ErrorCode interface

        ```java
        public interface BaseErrorInfoInterface {
        /** 错误码*/
        int getCode();

        /** 错误描述*/
        String getMessage();
        }
    3. 实现一个错误码枚举类并实现接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      public enum CommonEnum implements BaseErrorInfoInterface{
      BODY_NOT_MATCH(400,"请求的数据格式不符!"),
      SIGNATURE_NOT_MATCH(401,"请求错误!"),
      NOT_FOUND(404, "未找到该资源!"),
      INTERNAL_SERVER_ERROR(500, "服务器内部错误!"),
      SERVER_BUSY(503,"服务器正忙,请稍后再试!");

      /**错误码*/
      private final int code;
      /**
      * 错误消息
      */
      private final String message;

      CommonEnum(int code, String message) {
      this.code = code;
      this.message = message;
      }
      /**
      * 错误码
      */
      @Override
      public int getCode() {
      return code;
      }
      /**
      * 错误描述
      */
      @Override
      public String getMessage() {
      return message;
      }
      }
    4. 然后我们在来自定义一个异常类,用于处理我们发生的业务异常和内部异常。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      public class BizException extends RuntimeException{
      /**
      * 错误码
      */
      protected int errorCode;
      /**
      * 错误信息
      */
      protected String errorMsg;
      //默认无参构造方法
      public BizException() {
      super();
      }

      public BizException(BaseErrorInfoInterface errorInfoInterface){
      super(errorInfoInterface.getMessage());
      this.errorCode = errorInfoInterface.getCode();
      this.errorMsg = errorInfoInterface.getMessage();
      }

      public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
      super(errorInfoInterface.getMessage(), cause);
      this.errorCode = errorInfoInterface.getCode();
      this.errorMsg = errorInfoInterface.getMessage();
      }

      public BizException(String errorMsg){
      super(errorMsg);
      this.errorMsg = errorMsg;
      }

      public BizException(int errorCode, String errorMsg) {
      super(errorMsg);
      this.errorCode = errorCode;
      this.errorMsg = errorMsg;
      }

      public BizException(int errorCode, String errorMsg, Throwable cause) {
      super(errorMsg, cause);
      this.errorCode = errorCode;
      this.errorMsg = errorMsg;
      }

      public int getErrorCode() {
      return errorCode;
      }

      public void setErrorCode(int errorCode) {
      this.errorCode = errorCode;
      }

      public String getErrorMsg() {
      return errorMsg;
      }

      public void setErrorMsg(String errorMsg) {
      this.errorMsg = errorMsg;
      }
      //todo 对性能的影响
      @Override
      public Throwable fillInStackTrace() {
      return this;
      }

      @Override
      public String toString() {
      return "BizException{" +
      "errorCode=" + errorCode +
      ", errorMsg='" + errorMsg + '\'' +
      '}';
      }
      }
    5. 自定义全局捕获处理异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      @ControllerAdvice(annotations = Controller.class)
      @RestControllerAdvice
      @Slf4j
      public class ExceptionAdvice {

      @ExceptionHandler(ArithmeticException.class)
      public String doHandleArithmeticException(ArithmeticException e){
      //遍历堆栈信息并储存
      for (StackTraceElement element:e.getStackTrace()){
      log.error(element.toString());
      }
      return "计算过程中出现了异常,异常信息为"+e.getMessage();
      }

      /**
      * 处理自定义的业务异常
      * @param req
      * @param e
      * @return
      */
      @ExceptionHandler(value = BizException.class)
      @ResponseBody
      public ResultBody bizExceptionHandler(HttpServletRequest req, BizException e){
      log.error("发生业务异常!原因是:{}",e.getErrorMsg());
      return ResultBody.error(e.getErrorCode(),e.getErrorMsg());
      }

      /**
      * 处理空指针的异常
      * @param req
      * @param e
      * @return
      */
      @ExceptionHandler(value =NullPointerException.class)
      @ResponseBody
      public ResultBody exceptionHandler(HttpServletRequest req, NullPointerException e){
      log.error("发生空指针异常!原因是:",e);
      return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
      }

      //处理其他异常
      @ExceptionHandler({Exception.class})
      public void handleException(Exception e, HttpServletRequest httpRequest, HttpServletResponse response) throws IOException {
      log.error("服务器发生异常",e.getMessage());
      //遍历堆栈信息并储存
      for (StackTraceElement element:e.getStackTrace()){
      log.error(element.toString());
      }
      String requestHeader = httpRequest.getHeader("x-requested-with");
      if (requestHeader.equals("XMLHttpRequest")){
      response.setContentType("application/plain;charset=UTF-8");
      PrintWriter writer = response.getWriter();
      writer.write(CommunityUtils.getJSONString(1,"服务器异常"));
      }else {
      response.sendRedirect(httpRequest.getContextPath() + "/error");
      }
      }
      }
    6. 业务异常输出示例:

      1
      2
      2022-09-22 10:29:24,032 ERROR [http-nio-8081-exec-4] t.t.c.c.a.ExceptionAdvice [ExceptionAdvice.java:46] 发生业务异常!原因是:用户姓名不能为空!
      2022-09-22 10:29:24,036 WARN [http-nio-8081-exec-4] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver [AbstractHandlerExceptionResolver.java:208] Resolved [BizException{errorCode=-1, errorMsg='用户姓名不能为空!'}]

实际开发中遇到的问题和最佳实践

第一,注意捕获和处理异常的最佳实践。首先,不应该用 AOP 对所有方法进行统一异常处理,异常要么不捕获不处理,要么根据不同的业务逻辑、不同的异常类型进行精细化、针对性处理;其次,处理异常应该杜绝生吞,并确保异常栈信息得到保留;最后,如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息。

第二,务必小心 finally 代码块中资源回收逻辑,确保 finally 代码块不出现异常,内部把异常处理完毕,避免 finally 中的异常覆盖 try 中的异常;或者考虑使用 addSuppressed 方法把 finally 中的异常附加到 try 中的异常上,确保主异常信息不丢失。此外,使用实现了 AutoCloseable 接口的资源,务必使用 try-with-resources 模式来使用资源,确保资源可以正确释放,也同时确保异常可以正确处理。

第三,虽然在统一的地方定义收口所有的业务异常是一个不错的实践,但务必确保异常是每次 new 出来的,而不能使用一个预先定义的 static 字段存放异常,否则可能会引起栈信息的错乱。

第四,确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。

要注意的问题

  1. 在哪一层进行抛出?应该在哪里抛,抛什么异常。

A:比如 我们需要知道 参数为空报什么异常,对其进行捕获统一处理;

2、所有异常被吃掉,带来的严重的后果;

-------------本文结束感谢您的阅读-------------