java旧系统重构需用异常链机制实现渐进式兼容:通过枚举定义结构化业务码、各层保留cause、全局处理器分离响应与日志、上下文注入异常对象,确保异常可追溯、易定位、强兼容。

Java旧系统重构期最怕“一改全崩”——新逻辑上线,老流程报错,监控里堆满未知异常,排查时发现业务码和底层错混成一团。关键不是不抛异常,而是让每个异常既说清“哪里不对”,又指明“为什么不对”。异常链机制就是干这个的:它把业务意图、技术根因、运行现场串成一条可追溯的线,支撑渐进式兼容。
用枚举定义带语义的状态码,不写字符串硬编码
状态码不是HTTP状态(如400/500),也不是随便起的"ERR_01"。它得是结构化、可维护、带业务含义的枚举:
-
OrderErrorCode.PAYMENT_TIMEOUT("PAY_001", "支付超时,请重试") -
InventoryErrorCode.STOCK_INSUFFICIENT("INV_003", "库存不足,当前剩余{0}件")
每个枚举值固定绑定code和message,支持i18n占位。Controller层按code映射HTTP状态(如PAY_001 → 408),但service和dao层完全不感知HTTP——它们只认枚举。这样,新老逻辑共存时,只要返回同一个枚举,前端就无需改解析逻辑。
每层封装都保留原始异常,不丢cause
旧系统里常见反模式:throw new BusinessException("下单失败")——message写了,但MySQL死锁在哪?Feign超时堆栈在哪?全没了。正确做法是显式传递cause:
- DAO层抛出
DataAccessException→ Service层构造BusinessException(PAYMENT_TIMEOUT, "支付失败", e) - 调第三方时捕获
HttpClientErrorException→ 封装为BusinessException(THIRD_PARTY_UNAVAILABLE, "支付渠道不可用", e)
检查所有自定义异常类是否提供(ErrorCode, String, Throwable)构造函数,并确保调用super(message, cause)。这样异常链不断,日志里能一路看到Root Cause。
全局处理器分离响应体与日志内容@RestControllerAdvice不是来兜底打印堆栈的,而是做协议转换:
- 响应体只给前端需要的字段:
{"code":"PAY_001","msg":"支付超时,请重试","timestamp":"2026-06-07T07:54:00Z"} - 日志必须打完整异常链:
log.error("支付异常[{}], traceId={}", ex.getErrorCode(), traceId, ex)(注意ex是第三个参数)
对NullPointerException等系统异常,统一降级为SYSTEM_ERROR枚举,不暴露技术路径。这样前端不被堆栈吓到,运维却能在ELK里精准定位MySQL死锁或证书过期。
把订单号、用户ID等上下文注入异常对象,不拼在message里
message要保持稳定可翻译,运行时现场信息(如orderNo=ORD20260607001, userId=8892)应存在异常对象的Map<string object> context</string>字段中:
- 构造异常时调用
ex.withContext("orderNo", "ORD20260607001").withContext("userId", 8892) - 全局日志器读取context并打点:
log.error("支付异常[{}], orderNo={}, userId={}, traceId={}", ex.getErrorCode(), ex.getContext("orderNo"), ex.getContext("userId"), traceId, ex)
这样既避免message污染,又让问题定位从“猜”变成“查”。
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











