易君召
发布于 2026-09-16 / 作者:易君召 / 1 阅读
0

使用日志记录优化异常处理机制的建议

核心思路:异常负责控制业务流程,日志负责留存现场信息;不要用日志替代异常抛出,也不要只抛异常不记日志,二者各司其职。下面分原则、实践规范、避坑点、落地建议。

一、日志与异常的职责边界(最关键)

  1. 异常:用于程序控制流

    • 向上抛出,让上层选择降级、重试、返回错误码、终止流程;

    • 异常本身携带错误类型、简短原因

  2. 日志:用于事后排查

    • 记录完整现场上下文,不干预业务流程;

    • 不是捕获异常就打印日志,避免重复打日志。

准则:就近捕获、分层打印;只在异常的 “处理边界” 记录日志,底层工具类只抛异常不打日志

二、日志内容规范:异常日志该记录什么

必包含字段(结构化日志优先,JSON 格式)

  • 时间戳、日志级别

  • 唯一链路标识:traceId / requestId(分布式系统必备,串联一次请求全链路)

  • 异常类型、异常 message、完整堆栈 stackTrace

  • 业务上下文参数:用户 ID、订单号、请求入参、资源 ID、机器 IP、线程名

  • 错误码(自定义业务错误码,区分系统异常 / 业务异常)

日志级别选择

  • ERROR系统级异常,不可预期,需要人工介入:空指针、数据库崩溃、IO 失败、第三方调用超时

  • WARN业务可预期异常,不需要紧急告警,仅观察:参数校验不通过、重复提交、用户不存在(正常业务拒绝)

  • ❌ 禁止用 INFO/DEBUG 记录异常堆栈;不要把业务报错打印成 ERROR,造成告警风暴。

区分:业务异常≠系统故障,比如用户输入手机号格式错误,属于正常业务校验,打 WARN,不打 ERROR,不触发告警。

三、分层异常处理 + 日志最佳实践

1. 底层(DAO / 工具类 / SDK 封装)

✅ 只抛出异常,不打印日志

  • 底层不知道上层如何处理(重试、降级、包装);

  • 直接打日志会导致同一次异常,多层重复打印多条堆栈。

// 不好:底层直接打日志
try{
    db.select();
}catch(SQLException e){
    log.error("查询失败",e); // ❌ 底层打印,上层重试会重复日志
    throw new BusinessException(e);
}

// 推荐:仅包装异常向上抛,不打印
try{
    db.select();
}catch(SQLException e){
    throw new BusinessException("数据库查询异常", e); // 保留cause原始异常
}

2. 中间服务层(Service)

按需捕获、包装异常,选择性记录日志

  • 如果捕获后直接重试 / 降级:可以打 WARN,记录上下文,不向上抛

  • 如果捕获后包装为业务异常继续向上抛:不打印堆栈,交给外层统一日志切面处理。

3. 外层入口层(Controller / 消息消费者 / 定时任务)【日志记录主入口】

统一捕获、统一记录异常日志(AOP / 全局异常处理器)

  • 这是一次请求的边界,在这里打印完整异常 + 全链路上下文;

  • 全局异常处理器:记录日志 → 包装返回给前端 / 调用方;

  • 优点:集中管控,避免散落的 try-catch 日志,防止漏打、重复打。

重要:包装异常时,保留原始 cause,不要丢底层堆栈,否则日志看不到根因。 new BusinessException("业务提示", originalException);

四、try-catch 日志避坑(高频错误)

  1. catch(Exception e) { log.error("出错了"); } 只写描述,不传 e,丢失堆栈,完全无法定位代码行 ✅ log.error("订单查询失败,orderId:{}", orderId, e);

  2. ❌ 吞掉异常(空 catch)

    try { ... } catch (Exception e) {} // 静默失败,没有日志,灾难级
    

    必须至少打日志,或向上抛出;除非明确知道这个异常可以安全忽略。

  3. ❌ 重复记录同一个异常 底层打一次,service 又打,controller 再打,多条相同堆栈,干扰排查

  4. ❌ 日志字符串拼接异常消息,不要 e.getMessage() 拼字符串,直接使用日志参数 + 传入异常对象

  5. ❌ 敏感信息明文写入日志:手机号、身份证、密码、银行卡,需要脱敏

五、结构化日志 & 检索优化建议

  1. 使用 JSON 结构化日志,而不是纯文本字符串,方便 ELK/Loki 检索过滤

    {"traceId":"xxx","level":"ERROR","orderId":"123","msg":"更新订单失败","exception":"xxx堆栈"}
    
  2. 不要打印超长堆栈,限制堆栈行数;大循环内异常避免海量日志刷屏

  3. 区分根因异常和包装后的业务异常,日志保留完整 cause 链

  4. 增加采样策略:高频重复异常,做日志限流 / 采样,防止日志存储打爆

六、告警与日志联动

  • ERROR 级日志设置告警,但过滤业务异常,只对系统异常告警;

  • 日志增加标签,区分数据库、redis、第三方接口异常,实现告警分类;

  • 告警信息精简(只给摘要),详细堆栈留在日志平台,告警消息不贴完整堆栈。

七、业务异常 vs 系统异常 设计建议

  1. 业务异常(BusinessException):主动抛出,属于预期业务场景,带业务错误码,日志 WARN,不告警,对外返回友好提示。

  2. 系统异常(SystemException/RuntimeException):非预期故障,ERROR 日志,触发告警,对外返回通用提示,不暴露内部堆栈给前端。

八、额外优化点

  1. 不要在 finally 块中记录异常;finally 用于资源释放,异常记录放在 catch。

  2. 异步线程、线程池任务:主线程无法捕获子线程异常,子线程内部必须捕获并记录日志,否则异常直接丢失。

  3. 定时任务、MQ 消费者:必须全局 try-catch + 日志,否则任务异常丢失,消费丢失消息。

  4. 开发环境可以打印更多 DEBUG 上下文;生产环境控制日志量,避免性能损耗。

九、一句话总结规范

底层抛异常不打日志,外层全局处理器统一捕获并记录完整堆栈 + 链路上下文;业务预期异常打 WARN,非预期系统故障打 ERROR;永远保留原始 cause,不吞异常、不丢堆栈、脱敏敏感数据,避免重复日志。