一、一个常见的异常处理场景在 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)); }这段代码存在几个可优化点。下面我们通过完整的代码评审过程探讨异常日志记录的正确做法。二、第一次优化修正日志级别和冗余信息代码评审指出了两个问题日志级别不当在 catch 块中使用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, 身份证图片上传异常); }工程思维的体现这个方案的成功之处在于它没有在“成本”和“信息”之间二选一而是通过分类找到了平衡点对高频、低信息价值的业务异常做减法实现成本优化。对低频、高信息价值的系统异常做加法保障运维能力。这才是对“成本意识”的完整理解——成本不仅仅是存储开销更包括潜在的故障排查时间成本。后者往往比前者高得多。七、异常日志记录最佳实践总结1. 合理选择日志级别ERROR用于系统异常、不可恢复错误、第三方服务调用失败。WARN用于可恢复异常、业务预期内但不希望发生的情况(如重试、降级)。INFO/DEBUG用于业务流程记录、调试信息一般不用于记录异常本身。2. 区分异常类型差异化处理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, 系统内部错误); }3. 平衡存储成本与排查效率对高频且稳定的业务异常可仅记录消息或采用采样日志。对系统异常、底层中间件异常、网络超时等必须保留完整堆栈。八、总结一次看似简单的catch块日志记录背后涉及了日志级别、格式规范、存储成本、排查效率、异常分类处理等多个工程权衡点。通过这次代码评审我们认识到避免机械执行与简单回退理解每处修改的真实影响和背后原因建立自己的决策框架而不是在几个选项间盲目摇摆。坚持分类处理原则通过区分业务异常与系统异常能从根本上统一“降低成本”和“保留信息”这两个目标。建立全局成本观日志存储成本远低于因信息缺失导致的问题排查时间成本和系统稳定性风险。好的设计正是在多重约束中寻找最优解。【碎碎念一番】良好的异常日志实践是构建可观测、易维护的系统的基石。在每次编写try-catch时多花几秒钟思考如何记录异常就是在为未来的自己和团队节省大量的排查时间。
推敲见文章:从 `try..catch` 看异常日志打印的正确姿势
发布时间:2026/6/10 14:46:23
一、一个常见的异常处理场景在 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)); }这段代码存在几个可优化点。下面我们通过完整的代码评审过程探讨异常日志记录的正确做法。二、第一次优化修正日志级别和冗余信息代码评审指出了两个问题日志级别不当在 catch 块中使用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, 身份证图片上传异常); }工程思维的体现这个方案的成功之处在于它没有在“成本”和“信息”之间二选一而是通过分类找到了平衡点对高频、低信息价值的业务异常做减法实现成本优化。对低频、高信息价值的系统异常做加法保障运维能力。这才是对“成本意识”的完整理解——成本不仅仅是存储开销更包括潜在的故障排查时间成本。后者往往比前者高得多。七、异常日志记录最佳实践总结1. 合理选择日志级别ERROR用于系统异常、不可恢复错误、第三方服务调用失败。WARN用于可恢复异常、业务预期内但不希望发生的情况(如重试、降级)。INFO/DEBUG用于业务流程记录、调试信息一般不用于记录异常本身。2. 区分异常类型差异化处理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, 系统内部错误); }3. 平衡存储成本与排查效率对高频且稳定的业务异常可仅记录消息或采用采样日志。对系统异常、底层中间件异常、网络超时等必须保留完整堆栈。八、总结一次看似简单的catch块日志记录背后涉及了日志级别、格式规范、存储成本、排查效率、异常分类处理等多个工程权衡点。通过这次代码评审我们认识到避免机械执行与简单回退理解每处修改的真实影响和背后原因建立自己的决策框架而不是在几个选项间盲目摇摆。坚持分类处理原则通过区分业务异常与系统异常能从根本上统一“降低成本”和“保留信息”这两个目标。建立全局成本观日志存储成本远低于因信息缺失导致的问题排查时间成本和系统稳定性风险。好的设计正是在多重约束中寻找最优解。【碎碎念一番】良好的异常日志实践是构建可观测、易维护的系统的基石。在每次编写try-catch时多花几秒钟思考如何记录异常就是在为未来的自己和团队节省大量的排查时间。