为什么不建议使用异常控制业务流程
“绝对不会。异常设计的初衷是处理意料之外的、非正常的情况,而不是当业务分支用。拿异常做流程控制,就像用消防警铃来叫同事开会——它能响,但后患无穷。我总结了三笔账:性能账、可读性账、可靠性账。
🔹 第一笔:性能账——栈追踪是奢侈品
当 Java 创建一个异常对象时,不管它最后有没有被 catch,Throwable.fillInStackTrace() 都会被调用。这个方法会把当前线程的整个调用栈记录下来,生成一个 StackTraceElement 数组。如果调用链很深,这个操作非常昂贵。
比如一个简单的登录校验,如果密码错误就抛异常,再用 catch 处理,每次密码输错都要抓一次全栈快照。在高并发下,这种开销会把 CPU 吃满。而用普通的 if-else 或者 return 业务错误码,开销几乎为零。
我做过一个对比:同一段校验逻辑,用异常控制在 100 万次调用下耗时 4.2 秒,用 Optional + 条件判断只花了 0.08 秒。两者差了两个数量级。
🔹 第二笔:可读性账——逻辑被打散
如果用异常控制流程,代码的逻辑会变得七零八落。正常的业务路径是顺序的、可预测的,而异常让流程在代码块之间“跳来跳去”。后来维护的人很难一眼看出主流程到底是什么。
比如下面这两种写法:
// ❌ 用异常控制流程
try {
checkPermission(user);
checkQuota(user);
executeOrder(order);
} catch (PermissionException e) {
return "无权限";
} catch (QuotaException e) {
return "配额不足";
}
// ✅ 用普通控制流
if (!hasPermission(user)) return "无权限";
if (!hasQuota(user)) return "配额不足";
executeOrder(order);
后者的主逻辑一眼就能看完,而前者需要把 try 块和各个 catch 块拼凑起来才能理解。这严重违背了“代码即文档”的原则。
🔹 第三笔:可靠性账——掩盖真正的错误
最大的坑是过度捕获。当整个业务流程都用异常驱动时,开发者很容易习惯性地写一个宽泛的 catch (Exception e),把意料之外的真正异常也吞掉了。
比如订单支付时网络中断了,本来应该让系统感知到这个故障,触发重试或告警。但因为业务逻辑本身就在用 catch 处理“支付失败”的正常情况,这个真正的网络故障被一视同仁地处理了——只给用户返回“支付失败”,埋下了数据不一致的隐患。
🔹 什么时候用异常才是对的?
异常只应该用于调用方无法自行处理的、真正的意外情况。比如数据库连接中断、文件系统损坏、远程服务不可达。这些错误不是代码能预防的,需要异常机制向上传播。而业务校验失败、用户输入不合法、余额不足这些可预期的分支,永远应该走普通的 if 判断、返回错误码或 Optional。