惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
美团技术团队
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
M
MIT News - Artificial intelligence
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
A
About on SuperTechFans
Recent Announcements
Recent Announcements
D
Docker
Vercel News
Vercel News
Engineering at Meta
Engineering at Meta
腾讯CDC
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序?
推敲见文章:从 `try..catch` 看异常日志打印的正确姿势
buguge · 2026-05-27 · via 博客园 - buguge

一、一个常见的异常处理场景

在 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));
}

这段代码存在几个可优化点。下面我们通过完整的代码评审过程,探讨异常日志记录的正确做法。

二、第一次优化:修正日志级别和冗余信息

代码评审指出了两个问题:

  1. 日志级别不当:在 catch 块中使用 INFO 级别记录异常是不恰当的。异常通常表示错误或非预期情况,应使用 ERRORWARN 级别。
  2. 冗余上下文:异常堆栈通常已包含调用链信息,额外打印 reqId 可能非必需,尤其在高频接口中会增加日志体积。

开发者修改后的版本如下:

log.error("*****上传图片异常:{},", e);

三、第二次优化:修正日志格式问题

修改后的代码在格式上仍不够清晰:

log.error("*****上传图片异常:{},", e);

这里的格式字符串包含占位符 {},但实际传入了异常对象 e。在主流日志框架(如 Logback、Log4j2)中,这样写虽然能正常输出堆栈,但格式上易产生混淆。更清晰的写法是:

log.error("*****上传图片异常:", e);

四、第三次“优化”:平衡存储成本与排查效率

接下来,评审人从日常工作中强调的成本意识的角度提出:异常堆栈通常较长,全量打印会增加日志存储开销。

开发者修改后的版本如下:

} catch (Exception e) {
    log.error("*****上传图片异常:{}", ExceptionUtils.getMessage(e));
    ...
}

评审人:这一修改虽然降低了日志体积,但直接导致了堆栈跟踪信息的丢失,在后续排查问题时,只能看到异常消息,无法定位具体代码位置、调用链路和嵌套异常,显著增加了问题排查的难度。

五、哭笑不得:开发者回退到上一版本

开发者意识到“仅打印消息”会导致堆栈信息丢失后,没了主意。面对“要存储成本”和“要排查信息”这两个看似矛盾的要求,他采取了最简单的做法:直接回退到上一个“正确”的版本

log.error("*****上传图片异常:", e);

这个“回退”动作本身,恰恰暴露了问题

  1. 思考中断:开发者没有继续深入分析评审人提出的“成本”关切背后的合理成分(即:是否所有异常都需要完整堆栈?),而是选择退回到一个“安全”但未经深思的旧方案。
  2. 方案摇摆:代码在“完整堆栈”和“仅消息”两个极端之间摇摆,说明开发者尚未建立起处理这类权衡问题的稳定思路和决策框架
  3. 问题依旧:“成本”问题被搁置了,但并未被解决。如果未来日志量真的成为瓶颈,这个“回退”只是将问题推迟,而非化解。

六、正确的做法:区分异常类型,差异化处理

简单地回退或“一刀切”都是不行的。关键在于建立清晰的决策逻辑:区分异常类型,差异化处理。这能将“降低存储成本”和“保留排查线索”这两个目标统一起来。

异常类型 特点 日志策略建议 解决的核心矛盾
自定义业务异常 系统内定义,预期内,表示明确的业务规则违反(如“参数无效”、“余额不足”)。 堆栈价值低。通常直接抛出,无需记ERROR日志(可记WARN)。 显著降低成本:这类异常往往高频,不打印其堆栈能大幅减少日志量。
系统/运行时异常 NPEIO异常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 块日志记录,背后涉及了日志级别、格式规范、存储成本、排查效率、异常分类处理等多个工程权衡点。通过这次代码评审我们认识到:

  1. 避免机械执行与简单回退:理解每处修改的真实影响背后原因,建立自己的决策框架,而不是在几个选项间盲目摇摆。
  2. 坚持分类处理原则:通过区分业务异常与系统异常,能从根本上统一“降低成本”和“保留信息”这两个目标。
  3. 建立全局成本观:日志存储成本远低于因信息缺失导致的问题排查时间成本系统稳定性风险。好的设计正是在多重约束中寻找最优解。

【碎碎念一番】良好的异常日志实践,是构建可观测、易维护的系统的基石。在每次编写 try-catch 时,多花几秒钟思考如何记录异常,就是在为未来的自己和团队节省大量的排查时间。