【雨夜】业务中 自定义异常用 Exception 还是 RuntimeException? 为什么?

2,011 阅读5分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

今天和同事 聊了下异常 相关的事,整理在此

 目前公司中使用的 自定义异常是 extend RuntimeException 

目录 思维导图

image.png

继承异常

我们在业务开发中 继承异常是extend RuntimeException 还是 Exception呢

一想 这肯定是 RuntimeException 啊,但是这是为什么呢? 选择 Exception 不行么?

RuntimeException和Exception区别

聊这个之前 我们需要确定我们的需求是什么

需求

需求是 自定义一个 业务中使用的异常

确定了需求 接下来我们就要分析

image.png

到这里 知道 我们业务开发 基本都是运行时异常 所以用 RuntimeException

到这里有一个疑问,我们平时 IO操作的时候 动不动就爆红 让我们throws 或者 try catch,是哪个属性做到的?

首先看一段代码

image.png

但是这个异常是因为createNewFile 方法 主动抛出的异常 和 是不是 RuntimeException 没关系啊

public boolean createNewFile() throws IOException {
    SecurityManager security = System.getSecurityManager();
    if (security != null) security.checkWrite(path);
    if (isInvalid()) {
        throw new IOException("Invalid file path");
    }
    return fs.createFileExclusively(path);
}

是有关系的,非RuntimeException 都是需要throws 或者try catch 但是这个不是自动的,是开发人员写代码的时候 需要做的,代表这个异常是 必须被检查的

而其他需要注意的 引用 程序设计语言原理 书中 第434页的一部分

image.png

这块对于业务中使用哪个 先留一个疑问 往下看

image.png

怎么才算其他RuntimeException 代码上有什么区别?

没有区别就是一个继承 RuntimeException,另一个继承 Exception,别的没有了

事务中拦截异常

我们到这 知道了继承异常应该用RuntimeException,但是我们应该知道 阿里规约手册 中有这么一段

1、让检查异常也回滚:你就需要在整个方法前加上@Transactional(rollbackFor=Exception.class)

2、让非检查异常不回滚:
需要加入@Transactional(notRollbackFor=RunTimeException.class)

3、不需要事务管理(or 日志丢失)
需要加入@Transactional(propagation=Propagation.NOT_SUPPORTED)

为什么这么设置?

Exception类下面除了runtimeException还有SQLException和ioException

如果方法没有抛出runtimeException 而是抛出 SQLExceptionioException那么事务是不会回滚的

那么这就结束了吗?在我们编码过程中,如果方法要抛出一些可检查异常时是需要throws进行显式指定异常类的

那么问题来了,我们都知道方法签名中默认是throws RuntimeException,已知SqlException不是RuntimeException的子类

小总结

@Transactional(notRollbackFor=RunTimeException.class) 是因为抛弃了 IO异常和 SQL异常等情况,所以 我们 应该用 Transactional(rollbackFor=Exception.class)

为什么 不是 rollbackFor = Throwable.class 呢

不需要显式指定 rollbackFor = Throwable.class ,因为如果发生错误,spring将默认回滚事务。**

原本就是除了 非RuntimeException 别的已经在事务管理中了

在其默认配置中,Spring Framework的事务基础结构代码仅标记事务对于运行时回滚,未经检查的异常;也就是说,抛出的异常是RuntimeException的实例或子类。  (错误也会 - 默认情况下会导致回滚) 。从事务方法抛出的已检查异常不会导致在默认配置中回滚。**

或者查看 DefaultTransactionAttribute

public boolean rollbackOn(Throwable ex) { 
        return (ex instanceof RuntimeException || ex instanceof Error); 
}

具体位置 docs.spring.io/spring-fram…

总结下来事务方面应该使用 @Transactional(rollbackFor=Exception.class)

写到这 你肯定想问 你百度一篇文档 就告诉我们 默认Error就在事务管理中? 你做过实验么?

image.png

小实验

 @Transactional(rollbackFor = Exception.class)
    public void a() {
        Version version = new Version();
        version.setVersionName("test0123120123");
        version.setProjectId(1L);
        versionService.save(version);
        try{
            int i = 1 /0;
        }catch (Exception e){
            //抛出Error 异常
            throw new OutOfMemoryError("test 内存溢出 error");
        }
    }

执行结果 保证了事务,数据库没有新增

到这里 你肯定有疑问

你对 rollbackFor 设置为 Exception,是不是原来的 事务拦截就失效了(Error 和 Runtime异常),到底是 覆盖之前的数据 还是 扩展原来的数据(Error 和 Runtime异常)

image.png

这是因为 触发了 下面的判断,初步得到结论 是 扩展

public boolean rollbackOn(Throwable ex) { 
        return (ex instanceof RuntimeException || ex instanceof Error); 
}

继续实验

if (winner == null) {
    //说明 rollbackFor 没有命中这个异常,执行默认的判断 (ex instanceof RuntimeException || ex instanceof Error)
    //如果是 Error 异常 走这个分支
    logger.trace("No relevant rollback rule found: applying default rules");
    return super.rollbackOn(ex);
} else {
    //说明 rollbackFor 命中了 这个异常
    return !(winner instanceof NoRollbackRuleAttribute);
}

到这 我们就要 往回分析 winner 是怎么赋值的呢?

image.png

额外的学习点

怎么判断异常

RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
if (this.rollbackRules != null) {
    Iterator var4 = this.rollbackRules.iterator();

    while(var4.hasNext()) {
        RollbackRuleAttribute rule = (RollbackRuleAttribute)var4.next();
        //获取这个异常的 int 等级 ,越小越优先
        int depth = rule.getDepth(ex);
        if (depth >= 0 && depth < deepest) {
            deepest = depth;
            winner = rule;
        }
    }
}

List RollbackRuleAttribute rollbackRules 的值是怎么初始化的

其实一句话 怎么从 @Transactional(rollbackFor=Exception.class) 变为 list对象的? 走的是SpringTransactionAnnotationParser 类中的 parseTransactionAnnotation 方法

image.png

小总结

一句话 对事务是否生效的判断 是 在 (ex instanceof RuntimeException || ex instanceof Error) 基础上,进行了 扩展的判断

其他中间件 对异常的应用场景 和 为什么?

先看nacos

//检查 异常
public class NacosException extends Exception {
}

在使用的时候

image.png

有throws 的 也有 try catch的

那么 是什么抛出Nacos 异常的呢?

private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
        throws NacosException {
    try {
        xxx
    } catch (Exception e) {
        throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
    }
    throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
}

rocketmq

先看一个 运行时异常

运行时异常
public class AclException extends RuntimeException {
}

使用

public static void verify(String netaddress, int index) {
    if (!AclUtils.isScope(netaddress, index)) {
        //运行的时候 发生异常
        throw new AclException(String.format("Netaddress examine scope Exception netaddress is %s", netaddress));
    }
}

再看一个 检查异常

public class MQClientException extends Exception {
}
throws MQClientException, RemotingException, MQBrokerException

使用

public TransactionSendResult sendMessageInTransaction(final Message msg,
    final LocalTransactionExecuter localTransactionExecuter, final Object arg)
    throws MQClientException {
    TransactionListener transactionListener = getCheckListener();
    if (null == localTransactionExecuter && null == transactionListener) {
        //如果为空 client 异常
        throw new MQClientException("tranExecutor is null", null);
    }
 }

总结

中间件中大部分都是 client 连接失败,远程连接超时,server端异常 这种,属于检查时异常,所以应该 extend Exception

但是

  1. 业务开发的时候 大部分都是 判断空,属于运行时异常 推荐 RunntimeException
  2. 检查时异常 需要一直 throws,从代码整洁度上 推荐 RunntimeException

总结

  1. 远程连接的使用 / 异常 使用检查时异常,让另一端能感受到异常
  2. 业务中的代码使用 运行时异常

我还是推荐业务使用 extends RuntimeException,其余的就需要根据业务场景选择了

思考

 ok 那你说 远程连接使用 检查时异常,那feign 属于远程rpc,他的异常就必须是 检查时异常么?为什么?