核心思路:异常负责控制业务流程,日志负责留存现场信息;不要用日志替代异常抛出,也不要只抛异常不记日志,二者各司其职。下面分原则、实践规范、避坑点、落地建议。
一、日志与异常的职责边界(最关键)
异常:用于程序控制流
向上抛出,让上层选择降级、重试、返回错误码、终止流程;
异常本身携带错误类型、简短原因。
日志:用于事后排查
记录完整现场上下文,不干预业务流程;
不是捕获异常就打印日志,避免重复打日志。
准则:就近捕获、分层打印;只在异常的 “处理边界” 记录日志,底层工具类只抛异常不打日志。
二、日志内容规范:异常日志该记录什么
必包含字段(结构化日志优先,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 日志避坑(高频错误)
❌
catch(Exception e) { log.error("出错了"); }只写描述,不传 e,丢失堆栈,完全无法定位代码行 ✅log.error("订单查询失败,orderId:{}", orderId, e);❌ 吞掉异常(空 catch)
try { ... } catch (Exception e) {} // 静默失败,没有日志,灾难级必须至少打日志,或向上抛出;除非明确知道这个异常可以安全忽略。
❌ 重复记录同一个异常 底层打一次,service 又打,controller 再打,多条相同堆栈,干扰排查
❌ 日志字符串拼接异常消息,不要
e.getMessage()拼字符串,直接使用日志参数 + 传入异常对象❌ 敏感信息明文写入日志:手机号、身份证、密码、银行卡,需要脱敏
五、结构化日志 & 检索优化建议
使用 JSON 结构化日志,而不是纯文本字符串,方便 ELK/Loki 检索过滤
{"traceId":"xxx","level":"ERROR","orderId":"123","msg":"更新订单失败","exception":"xxx堆栈"}不要打印超长堆栈,限制堆栈行数;大循环内异常避免海量日志刷屏
区分根因异常和包装后的业务异常,日志保留完整 cause 链
增加采样策略:高频重复异常,做日志限流 / 采样,防止日志存储打爆
六、告警与日志联动
ERROR 级日志设置告警,但过滤业务异常,只对系统异常告警;
日志增加标签,区分数据库、redis、第三方接口异常,实现告警分类;
告警信息精简(只给摘要),详细堆栈留在日志平台,告警消息不贴完整堆栈。
七、业务异常 vs 系统异常 设计建议
业务异常(BusinessException):主动抛出,属于预期业务场景,带业务错误码,日志 WARN,不告警,对外返回友好提示。
系统异常(SystemException/RuntimeException):非预期故障,ERROR 日志,触发告警,对外返回通用提示,不暴露内部堆栈给前端。
八、额外优化点
不要在 finally 块中记录异常;finally 用于资源释放,异常记录放在 catch。
异步线程、线程池任务:主线程无法捕获子线程异常,子线程内部必须捕获并记录日志,否则异常直接丢失。
定时任务、MQ 消费者:必须全局 try-catch + 日志,否则任务异常丢失,消费丢失消息。
开发环境可以打印更多 DEBUG 上下文;生产环境控制日志量,避免性能损耗。
九、一句话总结规范
底层抛异常不打日志,外层全局处理器统一捕获并记录完整堆栈 + 链路上下文;业务预期异常打 WARN,非预期系统故障打 ERROR;永远保留原始 cause,不吞异常、不丢堆栈、脱敏敏感数据,避免重复日志。