SpringBoot 如何优雅的进行全局异常处理?

一、前言

Java中处理异常并不是一个简单的事情,不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。

最近在需求开发的过程中发现一些值得注意的问题,特别是在对Java异常处理的时候,比如有的同事对每个方法都进行 try-catch、在进行 IO 操作时忘记在 finally 块中关闭连接资源、有的同事无论什么异常直接捕获Exception、有的在事务代码里捕获过异常没有抛出,导致事务没有回滚等等问题。

回想自己对java的异常处理也不是特别清楚,便查阅了一些资料将异常相关内容记录下来,希望对读者有所帮助。

二、java异常说明

SpringBoot 如何优雅的进行全局异常处理?

2.1 异常分类:

  • Thorwable类 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。

  • Exception 是程序本身可以处理的异常,其中异常类又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非检查异常(Unchecked Exception)和检查异常(Checked Exception)。

  • Error(错误)是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。Error类及其子类也是非检查异常。

2.2 检查异常和非检查异常:

  • 可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception中的RuntimeException及RuntimeException的子类以外,其他的Exception类及其子类(例如:IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

  • 不可查异常(编译器不要求强制处置的异常): 包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。

说明:检查异常和非检查异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常:

  • 检查异常导致异常在方法调用链上显式传递,而且一旦底层接口的检查异常声明发生变化,会导致整个调用链代码更改。
  • 使用非检查异常不会影响方法签名,而且调用方可以自由决定何时何地捕获和处理异常。

建议使用非检查异常让代码更加简洁,而且更容易保持接口的稳定性。

三、java异常不规范举例

软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try {...} catch {...} finally {...} 代码块,不仅有大量的冗余代码,而且还影响代码的可读性。稍有不慎,可能引发线上bug。

@RestController
@AllArgsConstructor
@RequestMapping("/maintenancenotice")
@Api(value = "maintenancenotice", tags = "通知公告管理")
public class MaintenanceNoticeController {

 private final MaintenanceNoticeService maintenanceNoticeService;

 /**
  * 分页查询
  *
  * @param page              分页对象
  * @param maintenanceNotice 通知公告
  * @return
  */
 @ApiOperation(value = "分页查询", notes = "分页查询")
 @GetMapping("/page")
 public R getMaintenanceNoticePage(Page page, MaintenanceNotice maintenanceNotice) {
  try {
   return R.ok(maintenanceNoticeService.getPage(page, maintenanceNotice));
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }



 /**
  * 分页查询
  *
  * @param page              分页对象
  * @param maintenanceNotice 通知公告
  * @return
  */
 @ApiOperation(value = "分页查询", notes = "分页查询")
 @GetMapping("/workbench/page")
 public R getWorkbenchNoticePage(Page page, MaintenanceNotice maintenanceNotice) {
  try {
   try {
    return R.ok(maintenanceNoticeService.getWorkbenchNoticePage(page, maintenanceNotice));
   } catch (BusinessException e) {//重复处理
    throw new BusinessException(e);// 重复包装同样类型的异常信息
   }
  } catch (Exception e) {// 不应对所有类型的异常统一捕获,应该抽象出业务异常和系统异常,分别捕获
   throw new RuntimeException(e);
  }
 }

上面的示例,还只是在Controller层,如果是在Service层,可能会有更多的try catch代码块。这将会严重影响代码的可读性、“美观性”。

既然业务代码不显式地对异常进行捕获、处理,而异常肯定还是处理的,不然系统岂不是动不动就崩溃了,所以必须得有其他地方捕获并处理这些异常。

那么问题来了,如何优雅的处理各种异常?请往下看。

四、java 异常处理规范案例

4.1 阿里巴巴Java异常处理规约

SpringBoot 如何优雅的进行全局异常处理?
SpringBoot 如何优雅的进行全局异常处理?
SpringBoot 如何优雅的进行全局异常处理?
SpringBoot 如何优雅的进行全局异常处理?

4.2 异常处理最佳实践说明

1、使用try-with-resources语句可以避免需要手动释放资源的繁琐操作,使代码更加简洁和易于维护。

2、catch异常时,应当尽量确定catch的异常类型,而不是使用Exception来优化代码,因为Exception捕捉所有异常会导致程序失去了很多的信息,且不能根据具体异常类型采取相应的异常处理措施。捕获异常后要在注释中使用 @throw 进行说明。

3、在finally中应当释放资源,关闭IO对象,以确保资源的及时释放,避免资源泄漏。

4、捕获异常后使用描述性语言记录错误信息,如果是调用外部服务最好是包括入参和出参。

log.error("说明信息,异常信息:[{}]", e.getMessage(), e);

5、优先捕获具体异常。,不要忽略异常,异常捕获一定需要处理。

6、不要同时记录和抛出异常,因为异常会打印多次,正确的处理方式要么抛出异常要么记录异常,如果抛出异常,不要原封不动的抛出,可以自定义异常抛出。

7、对于自定义的异常类,应当明确异常原因、异常类型、异常描述等信息,以便在调试时能够快速定位和解决问题。

8、自定义异常不要丢弃原有异常,应该将原始异常传入自定义异常中。

throw MyException("my exception", e);

9、自定义异常尽量不要使用检查异常。

10、尽量避免使用捕捉所有异常语句,因为这会使代码变得难以调试和维护。

11、尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常。

12、为了避免重复输出异常日志,建议所有的异常日志都统一交由最上层输出。就算下层捕获到了某个异常,如非特殊情况,也不要将异常信息输出,应该交给最上层统一输出日志。

13、避免太多的try-catch嵌套:虽然try-catch可以避免程序出错,但是过多的try-catch嵌套会让代码变得复杂和难以维护。因此,应该尽可能避免过多的try-catch嵌套,简化代码结构。

14、在捕捉异常时应当遵循从最具体异常类型到最通用异常类型的顺序,以保证程序能够正确处理到每个异常。

15、对高风险的代码应当进行安全检查,以预先捕获可能出现的异常,提高程序的健壮性和可靠性。

4.3 try-with-resources语法糖

Java 7中引入的try-with-resources语法糖是一个非常有用的特性,它使得在代码中使用资源(例如文件或数据库连接)变得更加简单、方便和安全。

使用try-with-resources可以确保代码块执行完毕后,系统会自动关闭资源,从而避免资源泄漏和错误。

4.3.1 常规的try-catch示例

try {
    // 执行语句
    resource;
} catch (exceptionType e) {
    // 处理异常
} finally {
    // 执行清理操作
}

在try块中,如果发生异常,会被传递到相应的catch块进行处理。finally块中的语句正常情况下都会被执行,用于执行清理操作,例如关闭打开的流、释放占用的资源等。

4.3.2 try-with-resources示例

  public static void readFileStream(String path) {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            String line = br.readLine();
            while (line != null) {
                System.out.println(line);
                line = br.readLine();
            }
        }catch (IOException ioException){
            log.error(ioException.getMessage());
        }
    }

在这个例子中,我们使用try-with-resources语法糖来打开文件并读取它的内容。在try-with-resources语句块中,我们创建一个BufferedReader对象并将其包装在try语句的括号中,这样在try块执行结束后,它会自动关闭资源。

由于BufferedReader实现了AutoCloseable接口,因此它可以作为try-with-resources语句的一部分。

在try-with-resources语句中,除了BufferedReader,还有其他的资源对象可以使用,例如FileReader、InputStream、OutputStream、Socket等。

如果一个类实现了AutoCloseable接口,也可以在”try-with-resources”语句中使用。

资源必须实现java.lang.AutoCloseable接口或其子接口。AutoCloseable接口中定义了一个close()方法,当使用try-with-resources语法糖时,这个close()方法会在try块结束时自动调用。而替代了finally中关闭资源的功能,编译器为我们生成的异常处理过程如下:

  • try 块没有发生异常时,自动调用 close 方法,
  • try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中通过调用 Throwable.addSuppressed 来压制异常,但是你可以在catch块中,用 Throwable.getSuppressed 方法来获取到压制异常的数组。

五、项目中的异常处理实践

5.1 如何自定义异常

自定义异常类是指用户自己定义的、继承自Exception或其子类的异常类。使用自定义异常类的主要作用是提高程序的可读性和可维护性。自定义异常类可以将程序中可能出现的多种异常类型整合到一个自定义异常类中,从而使程序的异常处理更加规范、简单和方便。

在Java异常体系中定义了很多的异常,这些异常通常都是技术层面的异常,对于应用程序来说更多出现的是业务相关的异常,比如用户输入了一些不合法的参数,用户没有登录等,我们可以通过异常来对不同的业务问题进行分类,以便我们排查问题,所以需要自定义异常。那我们如何自定义异常呢?前面已经说了,在应用程序中尽量不要定义检查异常,应该定义非检查异常(运行时异常)。

在我看来,应用程序中定义的异常应该分为两类:

  • 业务异常:用户能够看懂并且能够处理的异常,比如用户没有登录,提示用户登录即可。
  • 系统异常:用户看不懂需要程序员处理的异常,比如网络连接超时,需要程序员排查相关问题。

下面是我设想的对于应用程序中的异常体系分类:

SpringBoot 如何优雅的进行全局异常处理?

5.2 以下是一些常见的自定义异常类:

  • IllegalArgumentException:表示方法的参数不合法;
  • IllegalStateException:表示对象的状态不合法;
  • NullPointerRuntimeException:表示空指针异常;
  • FileNotFoundException:表示文件不存在或无法打开;
  • SQLException:表示数据库访问错误;
  • NetworkException:表示网络连接错误;
  • TimeoutException:表示超时异常。

上述自定义异常类都是继承自Java的RuntimeException或Exception类的,我们可以根据自己的需要定义自己的异常类,以方便程序的异常处理。在定义自己的异常类时,需要注意一些规范,在构造器中添加有用的信息,并提供适当的访问器方法。另外,在使用自定义异常类时,需要提供必要的异常处理机制,以保证程序的健壮性与可靠性。

5.3 自定义异常类的用法如下:

  • 继承Exception或其子类,如RuntimeException等;
  • 添加一个无参的构造器,并在该构造器中调用父类的构造器,以便完成异常对象的创建;
  • 添加一个有参的构造器,并在构造器中调用父类的有参构造器,以便完成异常对象的创建,并为异常对象添加异常信息;
  • 可以添加一些自定义的方法来扩展自定义异常类的功能。

基于上述的说明,我认为应该这样来定义异常类,需要定义一个描述异常信息的枚举类,对于一些通用的异常信息可以在枚举中定义,如下所示:

package org.example.api.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;

/**
 * 异常状态枚举
 */
@Getter
@AllArgsConstructor
public enum ErrorCodeEnum {
    ERROR(9999, "系统异常"),
    HTTP_CONNECTION_OVERTIME(9998, "连接超时"),
    FREQUENTLY_REQUEST(9003, "操作频繁"),
    INVALID_RSA_KEY(9002, "超时失效"),
    TOKEN_TIMEOUT(9005, "token失效"),
    INVALID_PARAMS(9001, "非法参数"),
    INVALID_LOGIN_USER(9006, "该登录用户非企业内部用户,为企业外部联系人"),
    SIGN_ERROR(9000, "签名错误"),
    INVALID_STATUS(9004, "状态不符"),

    OK(200, "请求通过"),
    NO(201, "请求不通过"),
    TIP(202, "提示"),


    SUBMIT_NEED_CERTIFICATION(2006, "提交成功,请进行实名认证"),

    BLACK_LIST(3001, "账号黑名单"),
    UN_CERTIFICATED(3002, "转账账号未通过实名认证");

    /**
     * 状态码
     */
    private final Integer code;
    /**
     * 状态说明
     */
    private final String message;
    /**
     * 根据code获取message
     *
     * @param code 类型编码
     * @return Integer
     */
    public static String getMessage(Integer code) {
        return Arrays.stream(ErrorCodeEnum.class.getEnumConstants())
                .filter(e -> e.getCode().equals(code))
                .findFirst().map(ErrorCodeEnum::getMessage).orElseThrow(IllegalArgumentException::new);
    }
}

自定义系统异常类,其他类型的异常类似,只是异常的类名不同,如下代码所示:

/**
 * 系统异常类
 *
 */
package org.example.api.exception;

import org.example.api.enums.ErrorCodeEnum;

public class ErrorCodeException extends RuntimeException {


    private static final long serialVersionUID = -1795438611500271776L;
    /**
     * 错误码
     */
    private Integer code;

    public ErrorCodeException() {
        super();
    }

    public ErrorCodeException(Throwable cause) {
        super(cause);
    }

    public ErrorCodeException(String message) {
        super(message);
    }

    public ErrorCodeException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public ErrorCodeException(String message, Throwable cause) {
        super(message, cause);
    }

    public ErrorCodeException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
    public ErrorCodeException(ErrorCodeEnum errorCode, String msg) {
        super(msg);
        this.code = errorCode.getCode();
    }
    

    public ErrorCodeException(ErrorCodeEnum errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }


    public ErrorCodeException(ErrorCodeEnum errorCode, Throwable cause) {
        super(errorCode.getMessage(), cause);
        this.code = errorCode.getCode();
    }

    public Integer getCode() {
        return code;
    }


}

5.4 如何处理异常

我们应该尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常。前面的已经简单说明了一下如何处理异常,接下来将通过代码的方式讲解如何处理异常。

ErrorResponseMessage 对象的定义如下:

package org.example.api.exception;

import lombok.Data;
import org.example.api.enums.ErrorCodeEnum;

import java.io.Serializable;

/**
 * 异常简易返回信息
 */
@Data
public class ErrorResponseMessage implements Serializable {
    private static final long serialVersionUID = -3213302792125481771L;
    private Integer errorCode;
    private String errorMsg;

    public ErrorResponseMessage() {
    }

    public ErrorResponseMessage(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public ErrorResponseMessage(ErrorCodeEnum errorCode) {
        this.errorCode = errorCode.getCode();
        this.errorMsg = errorCode.getMessage();
    }

    public ErrorResponseMessage(ErrorCodeEnum errorCode, String errorMsg) {
        this.errorCode = errorCode.getCode();
        this.errorMsg = errorMsg;
    }
}

在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。

@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型。

下面我们通过 @RestControllerAdvice+@ExceptionHandler 实现基于异常处理器的http接口全局异常处理:

package org.example.api.exception;


import lombok.extern.slf4j.Slf4j;
import org.example.api.enums.ErrorCodeEnum;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.management.OperationsException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 管理平台全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class AppExceptionHandler {
    @ExceptionHandler
    public ErrorResponseMessage handleAndReturnData(HttpServletRequest request, @SuppressWarnings("unused") HttpServletResponse response, Exception ex) {
        ErrorResponseMessage message = new ErrorResponseMessage();
        if (ex instanceof ErrorCodeException) {
            ErrorCodeException e = (ErrorCodeException) ex;
            log.warn("[{}]接口异常ErrorCodeException[{}]", request.getRequestURI(), e);
            message.setErrorCode(e.getCode());
            message.setErrorMsg(e.getMessage());
            return message;
        }
        if (ex instanceof OperationsException) {
            OperationsException e = (OperationsException) ex;
            log.warn("[{}]接口异常OperationsException[{}]", request.getRequestURI(), e);
            message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
            message.setErrorMsg(e.getMessage());
            return message;
        }
        if (ex instanceof NullPointerException) {
            NullPointerException e = (NullPointerException) ex;
            log.error("[{}]接口异常-NullPointerException[{}]", request.getRequestURI(), e);
            message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
            message.setErrorMsg(e.getMessage());
            return message;
        }
        //处理未知异常
        log.error("[{}]系统异常-未知异常[{}]", request.getRequestURI(), ex);
        message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
        message.setErrorMsg("系统异常");
        return message;
    }
  

    /**
     * io异常处理
     *
     * @param request 请求
     * @param e       异常
     * @return SimpleMessage
     */
    @ExceptionHandler(value = IOException.class)
    public ErrorResponseMessage ioErrorHandler(HttpServletRequest request, IOException e) {
        ErrorResponseMessage message = new ErrorResponseMessage();
        String error = "java.io.IOException: Broken pipe";
        if (e.getMessage().contains(error)) {
            log.warn("[{}]接口异常[{}]", request.getRequestURI(), e.getMessage(), e);
            message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
            message.setErrorMsg(e.getMessage());
            return message;
        }
        log.error("[{}]接口异常[{}]", request.getRequestURI(), e.getMessage(), e);
        message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
        message.setErrorMsg(e.getMessage());
        return message;
    }
}

在 AppExceptionHandler 类中,@RestControllerAdvice = @ControllerAdvice + @ResponseBody ,如果有其他的异常需要处理,只需要在handleAndReturnData方法累加判断或者定义@ExceptionHandler注解的方法处理都行。

测试异常捕获结果:定义异常接口后,抛出自定义异常。在ApiPost上访问,可以看到已被全局异常拦截器拦截,返回了包装后的异常处理结果。

@RestController
@RequestMapping("/exceptionTest")
public class ExceptionTestController {

    @RequestMapping("/exceptionTest")
    public void exceptionTest() {
        throw new ErrorCodeException(ErrorCodeEnum.INVALID_PARAMS);
    }
}
SpringBoot 如何优雅的进行全局异常处理?

六、总结

在详细介绍异常处理时,本文首先解释了什么是异常Throwable。异常Throwable是Java中所有异常类的基类,它代表了一个在程序运行过程中发生的异常事件。异常Throwable可以分为两类:Error和Exception。Error通常表示系统级错误,比如JVM崩溃或内存不足等,这些是程序无法处理的;而Exception则是程序可以捕获并处理的异常。

在介绍完异常的基本概念后,本文通过举例的方式展示了项目开发中异常不规范代码的样子,例如:没有捕获异常、捕获异常后没有处理、异常描述不清晰等。这些不规范的代码不仅会影响代码的可读性和可维护性,还可能导致程序出现意外的错误。

接着,本文展示了阿里巴巴开发规约中关于异常处理的部分。阿里巴巴的Java开发规约中明确要求开发者在捕获异常时必须处理异常,而不是简单地忽略它。规约还强调了异常处理的规范性,包括异常的描述、异常的分类以及如何正确地抛出和捕获异常等。

按照自己的理解,本文总结了Java异常处理的最佳实践。首先,应该尽可能避免抛出检查异常,因为检查异常通常是由程序员的错误导致的;其次,捕获异常后必须处理异常,不能简单地忽略它;最后,应该使用清晰的异常描述来帮助开发者更好地理解问题所在。

在文章的最后一部分,本文阐述了如何在项目中自定义异常以及使用全局异常处理机制。在自定义异常时,应该根据业务需求定义具有明确含义的异常类,并确保每个异常类都具有清晰的文档注释。同时,可以使用全局异常处理机制来统一处理项目中发生的所有异常,从而提高代码的可读性和可维护性。

总之,本文通过对异常Throwable的介绍、项目开发中异常不规范代码的举例、阿里巴巴开发规约中关于异常处理的要求以及Java异常处理的最佳实践的总结,让读者对Java异常处理有了更深入的了解。同时,文章还阐述了如何在项目中自定义异常和使用全局异常处理机制,为读者在实际开发中提供了有益的参考。


原文始发于微信公众号(明月予我):SpringBoot 如何优雅的进行全局异常处理?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/272768.html

(0)
明月予我的头像明月予我bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!