在微服务开发中,服务间的异常传递是一个绕不开的话题。最近在排查一个跨服务调用的问题时,遇到了一个极其诡异的现象:提供端抛出的明明是自定义业务异常,消费端捕获到的却是冰冷的 RuntimeException。
今天就来复盘一下这个经典问题,带你扒开 Dubbo 的源码,看看它是如何"偷梁换柱"的,以及最优雅的解决姿势。
📍 案发现场还原
假设我们有两个服务:System(消费端)和 Order(提供端)。 在 Order 服务的接口实现中,当遇到业务规则校验不通过时,我们抛出了一个自定义的 ServiceException:
java
// Order 服务端逻辑
public OrderDTO createOrder(OrderReq req) {
if (req.getAmount() < 0) {
// 抛出自定义业务异常
throw new ServiceException(ErrorCode.INVALID_PARAM, "订单金额不能为负数");
}
// ...
}
本以为在 System 服务中可以优雅地 catch (ServiceException e) 并提取错误码,结果一运行,System 服务直接收到了一个 java.lang.RuntimeException,原本的错误码全丢了,堆栈信息也被包装得面目全非!
🕵️♂️ 捉虫:谁动了我的异常?
顺藤摸瓜,我们深入 Dubbo 的调用链路,终于在 Provider 端的拦截器链中找到了"真凶"------**Dubbo 内置的 ExceptionFilter**。
这是 Dubbo 官方默认开启的一个异常处理拦截器。它的设计初衷是非常严谨的防御性编程:为了防止消费端因为没有提供端的自定义异常类,而在反序列化时引发致命的 ClassNotFoundException,进而导致整个调用线程崩溃。
当 Order 服务抛出异常时,ExceptionFilter 会像安检员一样,拿着 6 条严格的规则进行校验:
- 是泛化调用吗?
- 是受检异常(Checked Exception)吗?
- 接口方法签名上是否
throws了这个异常? - 异常类和接口类在同一个 jar 包下吗?
- 是 JDK 自带的异常(
java.或javax.开头)吗? - 是 Dubbo 框架本身的内部异常吗?
真相大白: 我们的 ServiceException 是非受检异常,通常定义在单独的 common 包里,且大概率没有在 Dubbo 接口上显式 throws。因为它完美地避开了所有放行规则 ,Dubbo 触发了最终的兜底防御:提取原异常的 message,重新 new 了一个 RuntimeException 扔给消费端。
🚀 破局:一行配置的优雅反杀
既然知道了是官方默认的 ExceptionFilter 在作祟,且我们的微服务架构中,通常会有一个公共的 api.jar 或 common.jar(确保消费端一定能找到 ServiceException 这个类),那我们完全可以大胆地将这个保守的拦截器卸载掉。
在 Order 服务(提供端)的配置文件中,只需一行代码即可解决战斗:
yaml
dubbo:
provider:
# 使用前缀减号,精确狙击并剔除官方默认的 exception 拦截器
filter: "-exception"
配置解析: 这里的 -exception 语法是 Dubbo SPI 机制提供的高级特性。减号代表"移除",exception 是官方默认异常拦截器的扩展名。
重启服务后,ExceptionFilter 被成功移出调用链,Order 服务抛出的 ServiceException 终于得以保留原貌,跨越网络,完美反序列化并送达 System 服务手中!
💡 总结与避坑指南
遇到框架表现与预期不符时,不要急于写丑陋的补丁代码。源码之下无秘密,深入框架的核心执行流,往往能找到最四两拨千斤的解法。
⚠️ 终极警告: 使用 -exception 剔除默认过滤器的前提是:消费端的 Classpath 中必须有你的自定义异常类! 否则,摆脱了 RuntimeException 的你,将会迎面撞上更可怕的反序列化异常。在成熟的微服务团队中,将基础异常类下沉到全员共享的 API 包中,才是长治久安的王道。