SpringBoot中统一异常处理

1. 异常分类

  • Throwable类是所有异常的始祖,它有两个直接子类 Error 和 Exception:

    Error:仅在Java虚拟机中发生动态连接失败或其它的定位失败的时候抛出一个Error对象。一般由JVM处理,程序不用捕捉或抛出Error对象。

    Exception:程序在运行过程中出现的意外情况,可以被try-catch捕获和处理。

  • Java的异常(包括Exception和Error)通常分为可查异常(checked exceptions)和不可查异常(unchecked exceptions):

    UncheckedException:

    a. 包括Error与RuntimeException及其子类。

    b. 运行时异常需要程序员自己分析代码决定是否捕获和处理,比如空指针异常。

    CheckedException:
    a. 正确的程序在运行中,很容易出现的、情理可容的异常状况 。 可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常 状况,就必须采取某种方式进行处理。
    b. 除了Error和RuntimeException及其子类之外,其他的Exception类及其子类都属于可查异常。
    c. 这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

    下图展示了Java异常分类,其中粉红色背景异常是CheckedException类异常,紫色背景异常是UncheckedException异常。
    在这里插入图片描述

    常见的运行时异常(RuntimeException):

  • NullPointerException

    空指针异常,简单地说就是调用了未经初始化的对象或者是不存在的对象。

  • ClassNotFoundException

    类不存在异常,这里主要考虑一下类的名称和路径是否正确即可。

  • ArrayIndexOutOfBoundsException

    数组越界异常,在调用数组的时候一定要认真检查,看自己调用的下标是不是超出了数组的范围。

  • IndexOutOfBoundsException

    索引越界异常,当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。

  • NumberFormatException

    数字格式异常,当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。

  • OutOfMemoryException

    内存不足异常,通常发生于创建对象之时。

2. 异常如何处理

程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。

异常发生时,如果不进行处理的话,程序会终止运行。

对异常不做任何处理的代码示例:

@Test
public void test() throws Exception {
 if (true) {
     throw new Exception("产生异常咯");
 }
}

运行结果:
在这里插入图片描述

当异常发生时,为了使程序正常运行,就要对产生的异常进行一定处理。

处理异常有两种方式,一种是直接使用try-catch捕获并处理异常,另一种是将异常抛给上层,让上层进行处理。

使用try-catch捕获异常的代码示例:

@Test
public void test() {
 try {
     if (true) {
         throw new Exception("产生异常咯");
     }
 } catch (Exception e) {
     System.out.println("已成功捕获异常:" + e.getMessage());
 } finally {}
}

运行结果:
在这里插入图片描述
对异常进行处理后,程序依旧可以正常运行。

使用throws将异常抛给上层处理的代码示例:

public void test() throws Exception {
 if (true) {
     throw new Exception("产生异常咯");
 }
}

@Test
public void testUpper() {
 try {
     test();
 } catch (Exception e) {
     System.out.println("已成功捕获异常:"+e.getMessage());
 } finally {}
}

运行结果:
在这里插入图片描述
在test()方法定义时使用 throws Exception 声明异常,并抛出异常给上层方法testUpper()。然后在testUpper()方法里进行异常的捕获与处理,程序照样能正常运行。

3. 异常处理原则

异常处理一般原则

  • 能处理的异常尽早处理

    对于能明确知道要怎么处理的异常要第一时间处理掉。对于不知道要怎么处理的异常,要么直接向上抛出,要么转换成RuntimeException再向上抛出,让调用者处理。

  • throw抛出的异常要具体明确

    尽量不要用Exception捕获抛出所有异常。抛出异常时需要针对具体问题来抛出异常,抛出的异常要足够具体详细;在捕获异常时需要对捕获的异常进行细分,这时会有多个catch语句块,这几个catch块中间泛化程度越低的异常需要越放在前面捕获,泛化程度高的异常捕获放在后面,这样的好处是如果出现异常可以尽可能地明确异常的具体类型是什么。

  • 系统需要有统一异常处理机制

    系统需要有自己的一套统一异常处理的机制,如果系统使用Spring等框架的话可以非常简单地引入统一异常处理框架。另外在统一异常处理时一定要打印异常堆栈,不然的话问题可能就无从查起了。

Controller层异常处理

  • Controller 层不应再主动 throw 异常给上一层(前端),而是使用try-catch语句来捕获并处理异常。

  • Controller 层中调用Service层的方法要包裹在try-catch语句中。

  • 对于捕获的异常,说明这是我们预料之外的报错,应该使用 logger.error() 级别记录。

  • responseBean中设置返回值的状态与报错信息。

Service层异常处理

  • Service层的实现类需要加@BaseTransaction注解,当Service层中存在uncheckedException异常(即RunTimeException和Error)时,会自动回滚事务。

  • Service层的调用在Controller层中,并且该调用已经在try-catch语句块中,因此Service层中没必要再写try、catch语句,如果判断逻辑存在业务异常,应直接将该异常抛给Controller层即可,最终由Controller层统一捕获并处理。

  • 当Service层调用外部接口发生异常时,由于对方接口的数据没有办法回滚,可能需要将该异常在Service层中进行单独处理,这时候Service层中调用外部接口的代码需要放在try-catch语句中。

Dao层异常处理

  • Dao层不要手动throw或catch异常,而是统一交由Spring框架处理。

异常处理规约

  • 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它, 请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理 解的内容。
  • 不要在finally块中使用return。 原因是:try块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally块中的语句,如果此处存 在 return 语句,则在此直接返回,无情丢弃掉try块中的返回点。
  • 请使用 finally 来释放一些打开的资源,例如打开的文件、数据库连接等等。
  • 大部分情况下不建议在循环中进行异常处理,应该在循环外对异常进行捕获处理。
  • try语句块内要分清稳定代码和非稳定代码,对于稳定的不会出现异常的代码不要放到try语句块中

4. 自定义异常最佳实践

自定义异常首先定义一个基类异常 BaseException ,它继承了 RuntimeException 。

@Getter
public abstract class BaseException extends RuntimeException {
    
    private static final long serialVersionUID = -7296153112063518998L;
    protected String errorCode;
    protected String errorMessage;
    protected String errorRemind;

    BaseException(Throwable e, String... msg) {
        super(e);
        this.setFieldValueByMsgNumber(msg);
    }

    BaseException(String... msg) {
        this.setFieldValueByMsgNumber(msg);
    }

    public void setFieldValueByMsgNumber(String... msg) {
        if (msg.length > 0) {
            this.errorCode = msg[0];
        }
        if (msg.length > 1) {
            this.errorMessage = msg[1];
        }
        if (msg.length > 2) {
            this.errorRemind = msg[2];
        }
    }
}

那么BaseException为什么继承自RuntimeException而不是CheckedException呢。原因是Spring 事务控制默认只支持 RuntimeException 类的异常。

自定义的异常类,可以设置 ErrorCode 和 errorMessage 等字段,有了统一的 ErrorCode 和报错信息,排查和联调沟通就更容易了。

基于自定义的基类异常,定义一组子类异常,项目中常分为业务异常和系统执行异常两种。

// 业务异常
public class ServiceBizException extends BaseException { ... }
// 系统执行异常
public class ServiceAppException extends BaseException { ... }

业务异常:例如“用户输入的证件号不合法”、“相关校验失败”、“余额不足”等业务逻辑上的问题。

系统执行异常:例如网络超时、数据库异常、堆栈溢出、内存溢出等。

5. 未捕获异常处理

代码中有存在未捕获异常的可能性,其中未捕获异常的来源主要有两点:

  • 由于程序员的疏忽对抛出的异常未进行捕获,比如在Controller层的try-catch语句之外throw抛出异常。

  • 有可能会产生异常的代码块没有包裹在try-catch语句之中,程序运行时向上层抛出意料之外的异常。

不论是哪一点,直接将异常抛给前端是不规范的,因为前端可能会将该异常的细节(如sql语句的执行等)暴露出来。

争对未捕获异常SpringBoot框架的处理方法是:创建配置类 BaseAspect ,使用@AfterThrowing注解对未捕获的异常进行相关处理。

代码如下:

private static final String AOP_EXPRESSION= "execution(* com.sgm.dms..*.*(..))";

@AfterThrowing(pointcut = AOP_EXPRESSION, throwing = "ex")
public void afterException(JoinPoint jp, Throwable ex) {
    // 对异常进行部分处理
    ......
    // 打印未捕获异常日志
    LoggerFactory.getLogger(BaseAspect.class).error("AOP Exception Show", ex);
}

afterException方法对抛出未catch的异常进行了处理,并将该异常打印到日志。

其中execution(* com.sgm.dms….(…))中,第一个 “ * ” 号代表任意修饰符和任意返回值类型的方法,第二个 “ * ” 号代表该包下的所有子目录,第三个 “ * ” 号代表该路径下的所有类文件,第二个 “ … ” 号代表方法的参数为任意类型、任意个数。

使用@AfterThrowing注解虽然对异常进行了处理,但是不能完全处理异常,该异常还是会传到上一层调用者。即如果是Controller层抛出的未捕获的异常,即使在@AfterThrowing注解的方法中对该异常进行了部分处理,它还是会被传到前端。但是,当框架中存在统一异常处理机制时,还要经过统一异常处理机制的统一处理,然后才返回前端。

6. 统一异常处理

目前,我所了解的统一异常处理方法常见的有两种:

  • 创建一个用@RestControllerAdvice注解 的配置类,在该类中创建处理统一异常的方法,并且该方法必须使用@ExceptionHandler注解。

    代码示例:

    @RestControllerAdvice("com.example.demo")
    public class ControllerExceptionAdvice {
        private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionAdvice.class);
    
        @ExceptionHandler(RuntimeException.class)
        public Object exceptionHandler(RuntimeException e) {
            logger.error("产生异常:", e);
            return e.getMessage();
        }
    }
    

    使用这种统一异常处理方法的前提是在Controller层中必须使用@RequestMapping注解来映射http请求路径。如果Controller层中使用@Path来映射http请求路径,则该统一异常处理方法不起作用。

  • 如果SpringBoot项目中实现了Jersey的配置,那么可以使用Jersey框架的统一异常处理机制。

    使用Jersey框架的统一异常处理机制需要实现ExceptionMapper接口、重写该接口的toResponse方法。代码示例如下:

    public class BaseExceptionMapper implements ExceptionMapper<BaseException> {
    
        @Override
        public Response toResponse(BaseException exception) {
            int responseCode = 500;
            ExMessageResult resultMsg = new ExMessageResult(exception);
            AppExMessageResult messageResult = new AppExMessageResult(resultMsg);
            return Response.status(responseCode).entity(messageResult).type(MediaType.APPLICATION_JSON).build();
        }
    }
    

    在Jersey配置类中,将ExceptionMapper接口的实现类BaseExceptionMapper注册到Jersey容器中这样,Jersey会对BaseException类型的异常全局拦截处理。

    public abstract class JerseyApplication extends ResourceConfig {
    
        public JerseyApplication() {
            ...
            register(BaseExceptionMapper.class);
            ...
        }
    }
    

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值