异常处理应区分日志与抛出:仅在终止传播链时记录error日志,转换异常时不记录;必须抛出外部依赖失败类异常,编程错误应暴露;日志须带业务上下文并脱敏。

异常既不该无脑记录日志,也不该盲目抛出——关键看它是否属于「当前方法能处理的上下文」。
捕获后记录日志再抛出,通常是个坏主意
常见错误是这样写:
try {
doSomething();
} catch (IOException e) {
logger.error("doSomething failed", e);
throw e; // 或 throw new RuntimeException(e)
}
这会造成日志重复:上游若也记录,同一异常被刷两次;若上游继续包装再抛,堆栈还可能被截断。更严重的是,日志里出现「已知但未处理」的异常,会掩盖真正需要人工介入的问题。
- 只在你**终止异常传播链**时记录日志(比如在 controller 层兜底或 finally 清理资源)
- 若只是做转换(如
SQLException→DataAccessException),别记日志——交给最终消费者决定 - 用
logger.debug(..., e)替代error级别做诊断性输出,避免污染 error 日志
哪些异常必须抛出(而非吞掉或仅记录)
Java 异常分 checked 和 unchecked,但决策依据不是分类,而是语义:
立即学习“Java免费学习笔记(深入)”;
-
IOException、SQLException这类表示「外部依赖失败」,调用方大概率要重试、降级或提示用户,必须抛出(或封装后抛出) -
IllegalArgumentException、NullPointerException属于编程错误,应尽早暴露,通常不捕获——除非你在写 API 门面,需转为友好的BadRequestException - 吞掉异常(空
catch)或只打日志不抛出,等于把故障静默化,线上排查时根本找不到源头
日志内容要带上下文,不能只记异常类名
单独一行 logger.error("File read failed", e) 几乎没用。出问题时你不知道是哪个文件、什么参数、重试了几次。
- 记录关键业务变量:
logger.error("Failed to parse config for tenantId={}, version={}", tenantId, version, e) - 敏感字段(密码、token)必须脱敏,别让日志成漏洞出口
-
异步任务中,异常堆栈可能丢失线程上下文,建议在
Runnable或CompletableFuture的异常回调里补全 trace ID
最常被忽略的一点:日志和抛出不是二选一,而是分工。记录是为了可观测,抛出是为了可控流转。混淆这两者,系统就会一边疯狂刷 error 日志,一边在某个角落默默丢数据。










