异常处理应分三层:底层捕获并记录上下文后包装上抛,中间层标记逻辑缺陷并记录参数,外层统一拦截返回友好提示;日志需带traceid、脱敏敏感信息;多线程须显式处理异常。

直接上手学,别从概念堆里打转。重点不是记住“checked exception 有几种”,而是知道什么场景该捕获、什么该上抛、日志写在哪才真正有用。
先搞清异常处理的三层责任
很多初学者卡在“到底该不该 catch”——关键看谁该负责:
- 底层操作层(如 DAO、HTTP 调用):遇到 IOException、SQLException 等检查型异常,必须捕获并记录上下文(比如 URL、SQL、参数),然后包装成业务异常或重新抛出;
- 中间服务层(Service):不盲目吞异常,对运行时异常(如 NullPointerException、IllegalArgumentException)要标记为“逻辑缺陷”,记录堆栈+输入参数,再决定是否继续上抛;
- 最外层(Controller / 入口):统一用 @ControllerAdvice 拦截所有未处理异常,返回友好提示,并完成最终日志落盘——这里才是日志的“唯一出口”。
日志不是 print,得带上下文才有效
只写 logger.error("出错了") 和写 logger.error("订单创建失败,orderNo={}, userId={}, cause={}", orderNo, userId, e) 完全是两个世界:
- 用 SLF4J 的占位符写法,避免字符串拼接开销,也防止敏感字段被意外打印;
- 每条请求开头就用 MDC.put("traceId", UUID.randomUUID().toString()) 绑定链路 ID,后续所有日志自动带上 traceId;
- 对密码、身份证、手机号等字段,在写入日志前做脱敏(如 "138****1234"),不能靠“不记录”来规避风险。
多线程环境别掉坑
Runnable 或线程池里的任务一旦抛异常,默认静默终止,主线程完全不知情:
- 在 Thread 构造时显式设置 UncaughtExceptionHandler,或全局调用 Thread.setDefaultUncaughtExceptionHandler;
- 用 Callable + Future 时,异常藏在 ExecutionException.getCause() 里,必须调用 future.get() 才能暴露;
- 线程池(如 ThreadPoolTaskExecutor)可重写 afterExecute(Runnable r, Throwable t),统一捕获和记录子线程异常。
动手练几个典型场景
比看十篇理论更管用:
- 模拟一次数据库连接失败,观察 SQLException 怎么被捕获、重试、降级并记日志;
- 写一个空指针触发的 Service 方法,在 Controller 层统一拦截,返回 500 并输出 traceId;
- 起两个线程并发修改同一个 List,用 try-catch 包住 foreach,验证 ConcurrentModificationException 是否被正确捕获并记录。
不复杂但容易忽略。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











