4

浅学JAVA开发中异常处理

 1 year ago
source link: https://wendaoit.github.io/2023/02/27/2023-02-22-%E5%BC%80%E5%8F%91%E4%B8%AD%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

优雅处理异常

异常处理逻辑上的分类:

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

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

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

      一个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();
        }
        

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

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

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

        1. 定义一个错误码ErrorCode interface

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

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

      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. 然后我们在来自定义一个异常类,用于处理我们发生的业务异常和内部异常。

      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. 自定义全局捕获处理异常

      @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. 业务异常输出示例:

      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、所有异常被吃掉,带来的严重的后果;


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK