














在 Java 开发中,异常处理和日志记录是基础但容易出错的环节。最近在一次代码评审中,发现了下面这段典型的异常处理代码(其中的BizException是一个自定义异常):
try {
// 业务逻辑:上传身份证图片
// ...
} catch (Exception e) {
log.info("*****上传图片异常 reqId:{},", reqId, e);
if (e instanceof BizException) {
throw (BizException) e;
}
throw BizException.build(BossKgConstant.ERROR_6000,
"身份证图片上传异常:" + ExceptionUtils.getMessage(e));
}
这段代码存在几个可优化点。下面我们通过完整的代码评审过程,探讨异常日志记录的正确做法。
代码评审指出了两个问题:
INFO 级别记录异常是不恰当的。异常通常表示错误或非预期情况,应使用 ERROR 或 WARN 级别。reqId 可能非必需,尤其在高频接口中会增加日志体积。开发者修改后的版本如下:
log.error("*****上传图片异常:{},", e);
修改后的代码在格式上仍不够清晰:
log.error("*****上传图片异常:{},", e);
这里的格式字符串包含占位符 {},但实际传入了异常对象 e。在主流日志框架(如 Logback、Log4j2)中,这样写虽然能正常输出堆栈,但格式上易产生混淆。更清晰的写法是:
log.error("*****上传图片异常:", e);
接下来,评审人从日常工作中强调的成本意识的角度提出:异常堆栈通常较长,全量打印会增加日志存储开销。
开发者修改后的版本如下:
} catch (Exception e) {
log.error("*****上传图片异常:{}", ExceptionUtils.getMessage(e));
...
}
评审人:这一修改虽然降低了日志体积,但直接导致了堆栈跟踪信息的丢失,在后续排查问题时,只能看到异常消息,无法定位具体代码位置、调用链路和嵌套异常,显著增加了问题排查的难度。
开发者意识到“仅打印消息”会导致堆栈信息丢失后,没了主意。面对“要存储成本”和“要排查信息”这两个看似矛盾的要求,他采取了最简单的做法:直接回退到上一个“正确”的版本。
log.error("*****上传图片异常:", e);
这个“回退”动作本身,恰恰暴露了问题:
简单地回退或“一刀切”都是不行的。关键在于建立清晰的决策逻辑:区分异常类型,差异化处理。这能将“降低存储成本”和“保留排查线索”这两个目标统一起来。
| 异常类型 | 特点 | 日志策略建议 | 解决的核心矛盾 |
|---|---|---|---|
| 自定义业务异常 | 系统内定义,预期内,表示明确的业务规则违反(如“参数无效”、“余额不足”)。 | 堆栈价值低。通常直接抛出,无需记ERROR日志(可记WARN)。 |
显著降低成本:这类异常往往高频,不打印其堆栈能大幅减少日志量。 |
| 系统/运行时异常 | 如NPE、IO异常、DB连接异常、RPC超时等。 |
堆栈至关重要,必须记ERROR级别日志及完整堆栈。 |
保障可排查性:这类异常是线上问题的主要来源,堆栈是定位根因的生命线。 |
最终的实现方案如下:
try {
// 业务逻辑
// ...
} catch (Exception e) {
if (e instanceof BizException) {
// 1. 业务异常:低成本处理
// 直接抛出,通常无需记录ERROR日志。若需跟踪,可记WARN且仅记消息。
// log.warn("业务异常[code:{}]: {}", ((BizException)e).getCode(), e.getMessage());
throw (BizException) e;
}
// 2. 系统/运行时异常:高价值信息保留
// 必须记录ERROR和完整堆栈,这是付出的必要“成本”。
log.error("*****上传图片异常", e);
// 3. 统一对外暴露
// 将系统异常转换为对上游友好的业务异常。
throw BizException.build(BossKgConstant.ERROR_6000, "身份证图片上传异常");
}
工程思维的体现:
这个方案的成功之处在于,它没有在“成本”和“信息”之间二选一,而是通过分类找到了平衡点:
try {
// ...
} catch (BusinessException e) {
// 业务异常:可记录 WARN,通常直接抛出
log.warn("业务处理失败, code:{}, msg:{}", e.getCode(), e.getMessage());
throw e;
} catch (IOException | TimeoutException e) {
// 特定的系统异常:记录 ERROR 和堆栈,可附加上下文
log.error("IO操作失败 - 目标资源:{}", resourceId, e);
throw new BusinessException("SYS_ERROR", "系统繁忙,请重试", e);
} catch (Exception e) {
// 未知异常:必须记录 ERROR 和完整堆栈
log.error("未捕获的异常", e);
throw new BusinessException("SYS_ERROR", "系统内部错误");
}
一次看似简单的 catch 块日志记录,背后涉及了日志级别、格式规范、存储成本、排查效率、异常分类处理等多个工程权衡点。通过这次代码评审我们认识到:
【碎碎念一番】良好的异常日志实践,是构建可观测、易维护的系统的基石。在每次编写 try-catch 时,多花几秒钟思考如何记录异常,就是在为未来的自己和团队节省大量的排查时间。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。