Java异常体系万字笔记|字节码、性能、Lambda、CompletableFuture全梳理

在这里插入图片描述

简介:全方位拆解Java完整异常体系,从继承图谱、受检/非受检核心区别、JVM字节码底层、性能原理,到try-catch-finally、TWR资源关闭、异常链包装、Lambda/Stream异步异常、CompletableFuture高阶用法,搭配生产避坑规范、自定义异常模板、面试易错点速查表,适合面试复盘、生产排障、代码规范落地。

1. 继承体系全景图

Throwable
├── Error(系统级错误,不可恢复,业务不应捕获)
│   ├── VirtualMachineError(虚拟机内部错误父类)
│   │   ├── OutOfMemoryError(堆/元空间/直接内存/线程创建失败)
│   │   ├── StackOverflowError(方法栈过深/大量局部变量/递归过深)
│   │   └── InternalError(JVM 内部实现错误)
│   ├── LinkageError(类加载链接错误总父类)
│   │   ├── NoClassDefFoundError(编译期存在,运行时链接缺失Jar)
│   │   ├── ExceptionInInitializerError(静态代码块/静态变量初始化失败,原异常会作为cause嵌套)
│   │   ├── ClassFormatError / VerifyError(类文件格式/字节码校验失败)
│   │   └── UnsatisfiedLinkError(Native本地库加载失败,生产高频)
│   ├── AssertionError(assert断言失败,需JVM -ea参数开启,默认关闭)
│
└── Exception(应用级异常)
    ├── RuntimeException(非受检异常,Unchecked)
    │   ├── NullPointerException(NPE,Java14+提供精准报错堆栈)
    │   ├── IllegalArgumentException(非法参数)
    │   │   └── NumberFormatException(数字格式错误)
    │   ├── IndexOutOfBoundsException(越界)
    │   │   └── ArrayIndexOutOfBoundsException / StringIndexOutOfBoundsException
    │   ├── ClassCastException(强制类型转换失败)
    │   ├── ArithmeticException(算术异常,如除零)
    │   ├── IllegalStateException(状态非法)
    │   ├── UnsupportedOperationException(不支持的操作,如 Arrays.asList 的 add)
    │   ├── ConcurrentModificationException(快速失败,迭代时结构性修改)
    │   ├── NoSuchElementException(迭代器已无元素)
    │   ├── IllegalMonitorStateException(锁操作错误,wait/notify调用不当)
    │   ├── NegativeArraySizeException(数组长度为负)
    │   └── SecurityException(安全校验失败;JDK17起SecurityManager废弃,非SM场景仍适用)
    │
    └── 受检异常(Checked,编译期强制处理)
        ├── IOException
        │   ├── FileNotFoundException
        │   ├── SocketException
        │   └── EOFException
        ├── SQLException(JDBC 异常)
        ├── ReflectiveOperationException(反射操作异常总父类,Java 7+)
        │   ├── ClassNotFoundException(反射主动加载类失败)
        │   ├── InstantiationException / IllegalAccessException
        │   ├── InvocationTargetException(反射调用目标异常包装)
        │   └── NoSuchMethodException / NoSuchFieldException
        ├── InterruptedException(线程阻塞被中断)
        ├── ParseException(日期/文本解析)
        └── TimeoutException(超时)

💡高频面试辨析:
ClassNotFoundException:主动加载类失败(Class.forName),受检异常
NoClassDefFoundError:编译存在、运行时链接缺失依赖,Error级系统错误

2. 受检异常 vs 非受检异常

维度受检异常(Checked)非受检异常(Unchecked)
范围Exception 且非 RuntimeException所有Error、RuntimeException及其子类
编译期强制处理:try-catch 或 throws不强制编译检查
语义可预期、可恢复的外部风险编程逻辑错误、配置错误、不可恢复问题
示例IOException、SQLExceptionNPE、IllegalArgumentException
设计原则调用方必须面对并处理通过代码逻辑规避,无需强制捕获

2.1 核心底层本质(面试高频)

  1. 受检异常是 Javac 编译器的语法约束,JVM 运行时不会强制要求方法捕获或声明受检异常;但类文件的 Exceptions 方法属性会记录声明的受检异常,类加载阶段会做格式校验。
  2. 可通过字节码增强工具(如 Lombok @SneakyThrows)绕过编译期校验,直接在方法内抛出受检异常而无需声明。
  3. 所有异常最终都会触发栈展开与异常表匹配,运行时处理机制一致。

2.2 @SneakyThrows 补充(高频实用)

Lombok 的 @SneakyThrows 底层是在字节码层面直接抛出受检异常,绕开 javac 的编译检查。

  • 适用场景:Lambda 内受检异常包装、工具类简化异常声明;
  • 风险:破坏受检异常的契约,调用方无法感知异常,需确保上层有统一兜底处理。

2.3 函数式接口异常严谨说明

  • 标准函数式接口 Function/Consumer/Supplier 的抽象方法无 throws 声明,因此 Stream 链式调用中无法直接透传受检异常;
  • 若自定义函数式接口或 Callable 等原生接口声明了异常,Lambda 允许抛出对应受检异常,并非完全不支持。

2.4 现代框架的设计趋势

Spring、Hibernate、MyBatis 主流框架,统一将底层受检异常转为非受检异常,核心理由:

  1. 受检异常层层传递,污染方法签名,代码极度臃肿;
  2. 标准函数式接口不支持 throws,无法兼容函数式编程;
  3. 业务中绝大多数异常无法手动恢复,全局统一处理更优雅。

✨Effective Java 权威完整语义:
可恢复、需要调用方主动处理的异常 → 用受检异常
程序Bug、无法恢复的异常 → 用运行时异常
现代框架摒弃大量受检异常是工程取舍,并非完全否定受检异常设计。

2.5 受检异常的现实争议与中立视角

  1. 调用链污染:每层方法都需追加throws,破坏代码整洁性;
  2. 与函数式编程不兼容,Stream场景无法直接抛出;
  3. 开发者普遍粗暴 catch(Exception){} 吞异常,违背设计初衷。

客观补充:受检异常是Java的特色设计,初衷是强制开发者重视并处理可恢复的外部错误;现代工程的“去受检异常化”是业务场景下的权衡,并非设计本身的失败。

3. 异常底层机制(JVM 字节码与性能)

3.1 核心字节码指令

  • athrow:JVM字节码中显式抛出异常的核心指令,手动 throw 语句会编译为该指令;
  • NPE、数组越界、除零等JVM隐式异常,由虚拟机执行指令时检测到错误条件后直接抛出,不依赖方法内的 athrow 字节码。

3.2 字节码异常表(Exception Table)

class文件每个方法维护异常表,存储try-catch映射关系:

属性说明
start_pc / end_pctry块字节码偏移区间
handler_pccatch异常处理器字节码起始位置
catch_type异常类常量池索引;0代表捕获所有异常(catch-all),是编译器实现finally兜底的核心手段

补充机制:异常表采用顺序匹配规则,多catch要求“子类在前”本质是编译后异常表条目按顺序排列,优先匹配子类;multi-catch语法编译后会生成多个异常表条目,共享同一段异常处理器代码,减少字节码冗余。异常表与行号表(LineNumberTable)配合,保证堆栈能映射到准确的源码行号。

3.3 异常栈展开完整流程

异常抛出后,JVM会执行栈展开(Stack Unwinding)查找异常处理器:

  1. 先在当前方法的异常表中按顺序查找匹配的catch处理器;
  2. 找不到则弹出当前栈帧,清除局部变量与操作数栈,回到上层调用方法继续查找;
  3. 逐层向上遍历调用栈,直到找到匹配的catch块;
  4. 遍历完仍无匹配处理器,线程终止,默认向System.err输出完整堆栈。

3.4 finally 底层实现(完整修正)

现代JDK编译器无专属 finally 字节码指令,通过两种机制共同实现兜底:

  1. 正常/return出口:在所有正常退出路径、return 语句前复制 finally 代码块;
  2. 异常出口:通过异常表中 catch_type=0 的 catch-all 条目捕获所有异常,跳转到 finally 代码执行,执行完毕后重新抛出原异常。

因此 finally 中抛出新异常或执行 return,会直接覆盖原有异常和返回值;若 finally 仅修改对象属性、无 return/throw,则不会覆盖上层结果。

3.5 异常性能核心真相(破除最大误区)

✅ 无异常触发时,try-catch 几乎零性能开销
❌ 网传:try包裹代码会变慢(完全错误)

异常的主要性能开销来自异常抛出瞬间:JVM调用native方法 fillInStackTrace() 遍历线程所有栈帧,记录完整堆栈信息;其次还包括栈展开、异常表匹配等开销。

📊量化参考:基准测试显示,无异常时try-catch与普通代码性能差异通常在1%以内;高频抛出异常时,性能会下降10~100倍,具体取决于调用栈深度。

3.6 官方异常优化构造器(JDK7+)

protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
  • enableSuppression:是否开启异常抑制(TWR核心依赖)
  • writableStackTrace:是否生成堆栈信息,关闭可显著降低异常创建开销

3.7 高性能优化方案(面试+源码高频)

手段说明适用场景
静态异常复用预创建静态异常,规避new和堆栈填充Netty/Kafka高频快速失败场景
关闭堆栈生成writableStackTrace=false自定义业务异常,无需堆栈
重写fillInStackTrace空方法覆盖原生逻辑仅框架底层使用,业务禁止

⚠️ 超级生产大坑
可修改内部状态的静态复用异常,多线程并发下会污染cause、suppressed数组、堆栈行号,堆栈错乱串号,线上极难排查!
仅纯信号类、永不修改内部状态的异常可安全复用(如Netty的ClosedChannelException);业务代码严禁静态复用异常。

3.8 JIT编译优化严谨说明

  1. JIT 会将 catch 块标记为冷路径,不影响热路径的指令优化;频繁抛出异常会影响分支预测效果。
  2. 去优化(Deoptimization) 是JIT编译态退回解释态的机制,与 fillInStackTrace 堆栈填充是两个独立概念,无直接因果关系。
  3. 通常情况下JIT不会删除athrow指令、不会随意消除异常捕获语义;特殊情况:如果JIT通过数据流分析能证明try块内绝对不会抛出对应类型的异常,会将对应catch块作为死代码消除,同时移除异常表中的对应条目。
  4. 空 catch 块仅会优化掉空执行体,不会移除整个捕获机制,因为异常捕获本身会影响方法的栈展开行为。

4. try-catch-finally 深度解析

4.1 多catch规范 & multi-catch语法

普通多catch:子类在前,父类在后,否则编译报错

try {
    // 业务代码
} catch (FileNotFoundException e) {
    // 精准子类异常
} catch (IOException e) {
    // 父类异常兜底
}

Java7+ multi-catch

// 多个异常无父子继承关系,隐式final不可二次赋值
try {
    // 业务代码
} catch (FileNotFoundException | SQLException e) {
    // 编译类型为多个异常的最小公共父类(不一定是Exception)
}

硬性规则:multi-catch 异常类型禁止存在父子关系,直接编译报错。

4.2 finally 经典硬核坑与执行边界(面试必考)

  1. finally return 会覆盖 try/catch 所有返回值
  2. finally 抛异常会完全覆盖原有主异常,原异常永久丢失
  3. finally 中 return 会直接吞掉异常:若 try/catch 抛出异常,finally 执行 return 后,原异常会被彻底丢弃,方法正常返回值,不会向外抛出。
  4. try中return的底层逻辑:先计算return表达式的值,存入局部变量表的临时槽位;执行finally代码块后,再将临时槽位的值压入操作数栈执行返回指令。
  5. 返回值修改规则:基本类型无法被finally修改;引用类型无法修改引用地址,但可以修改堆内对象的内部属性——因为操作数栈暂存的是对象引用地址(值拷贝),finally通过该地址修改堆内对象,因此生效。
  6. finally 不执行的完整场景:
    • 调用System.exit()终止JVM(会触发关闭钩子);
    • 调用Runtime.getRuntime().halt()强制终止JVM(不触发关闭钩子);
    • 线程执行try块时被直接终止(如线程池shutdownNow、线程中断后退出);
    • 系统崩溃、断电、进程被强制杀死等操作系统层面终止。

4.3 方法重写 throws 规范

  1. 子类重写方法不能抛出更宽泛的受检异常;
  2. 子类可抛空、或抛出父类异常的子类;
  3. 非受检异常无约束,但不推荐滥用;
  4. 子类构造器必须通过 throws 声明父类构造器抛出的受检异常——因为 super() 必须位于方法体第一行,无法通过 try-catch 包裹父构造调用来内部捕获。

5. try-with-resources(TWR)资源自动关闭

5.1 核心接口区别

  • AutoCloseable:顶层接口,close() 可抛出任意 Exception
  • Closeable:继承自 AutoCloseable,close() 强制抛出IOException,约束更严格;TWR 诞生的初衷之一就是弱化该限制

5.2 语法特性

// JDK7 标准写法
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    return br.readLine();
}

// JDK9 增强:支持外部有效final变量
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
try (br) {
    return br.readLine();
}

5.3 标准执行顺序(符合JLS规范)

  1. try 代码块执行
  2. 在编译器生成的隐式finally块中自动逆序关闭所有资源;若 try 块已抛出主异常且 close() 也抛出异常,则 close 异常会被加入主异常的 suppressed 数组
  3. 进入 catch 块捕获异常
  4. 最后执行显式 finally 块

依据JLS 14.20.3扩展规范:带catch/finally的TWR会被编译为嵌套结构,资源关闭在内层隐式finally中执行,外层catch捕获异常,外层finally最后执行。

5.4 边界细节与核心优势

  1. 关闭顺序:与声明顺序逆序关闭
  2. null资源安全:TWR底层自带非空判断,关闭null资源不会抛NPE;但try块内操作null资源、资源初始化失败,依然正常抛异常
  3. 资源初始化异常安全:资源声明列表中,若第N个资源初始化抛出异常,前面所有已初始化成功的资源会被自动逆序关闭,不会发生资源泄漏,这是TWR相比手动finally的核心优势之一
  4. 异常抑制机制(Suppressed)
    • try块异常为主异常
    • close()异常被抑制,存入getSuppressed()数组
    • 若try正常结束、仅close抛异常,该异常直接抛出,无抑制异常
  5. 多资源close均报错规则:若try块已抛出主异常,其余close异常全部被抑制;若try块无异常,则第一个close异常作为主异常,后续close异常被抑制
  6. 自定义AutoCloseable抛受检异常,外层必须处理或throws,否则编译报错
  7. 除TWR自动生成外,可手动调用 Throwable.addSuppressed() 添加抑制异常,适用于多资源并行关闭的自定义场景。

💡传统手动finally的缺陷:如果在finally中依次关闭多个资源,第一个资源关闭抛出异常会导致后续资源关闭代码被跳过,造成资源泄漏;这也是JDK7推出TWR的核心原因之一。

5.5 suppressed 异常排查实战代码

// 打印主异常 + 所有被抑制异常,完整排查链路
log.error("业务异常: ", e);
// 遍历 suppressed 异常,排查隐藏根因
for (Throwable suppressed : e.getSuppressed()) {
    log.error("资源关闭被抑制异常: ", suppressed);
}

🚨生产重大坑:线上大量故障根因藏在 suppressed 抑制异常中,排查必须遍历打印!

6. 异常链、异常包装与Lambda高阶处理

6.1 异常转换规范

底层原始异常禁止直接透传,必须转为业务异常并保留cause根源:

// 标准规范写法
try {
    // 底层IO/数据库操作
} catch (IOException e) {
    throw new BusinessException("订单解析失败:" + orderId, e);
}

禁止:只抛新异常、丢失原始堆栈,等于彻底丢失排障线索

6.2 异常链核心规则

  1. initCause() 仅可调用一次;构造器传入 cause 本质是内部调用了 initCause(),因此构造时已指定cause的异常,不可再调用该方法,否则抛出 IllegalStateException。
  2. 两套独立机制:
    • 异常链(getCause()):用于保留业务根因,层层包装传递;
    • 异常抑制(getSuppressed()):用于保留并行发生的次要异常(如TWR资源关闭异常)。

6.3 四大经典包装异常辨析

异常类场景核心说明
InvocationTargetException反射Method.invoke包装被调用方法真实异常,getCause获取根因
UndeclaredThrowableExceptionJDK动态代理包装接口未声明的受检异常
ExecutionExceptionFuture.get()同步获取异步任务异常(受检)
CompletionExceptionCompletableFuture异步流水线异常包装(非受检)

6.4 CompletableFuture 异常终极严谨语义

  1. 上游异常会跳过所有普通链式方法(thenApply/thenAccept等)直接向下传递,直到遇到异常处理算子。
  2. exceptionally:仅上游异常时执行降级;自身抛异常依然会包装为CompletionException继续传播。
  3. handle:成功/异常全部执行,可吞异常、自定义返回值。
  4. whenComplete:不修改、不包装上游原始异常,原样透传结果;若回调内部主动抛出新异常,返回的新阶段会以该新异常完成,原异常/原返回值都会被直接丢弃(而非抑制)。

高阶补充(JDK12+):

  • exceptionallyCompose:异常时返回新的CompletionStage,支持异步降级编排
  • orTimeout / completeOnTimeout:超时异常处理,生产异步编排高频使用
  • 所有异常算子均有对应的Async异步版本(如handleAsync),可指定线程池执行处理逻辑

6.5 高频面试:get() vs join() 完整区别

  1. get():抛出受检异常(ExecutionException、InterruptedException),必须显式 try-catch
  2. join():抛出非受检异常(正常异常为 CompletionException,任务取消时抛 CancellationException),无需强制捕获

6.6 线程池异常核心坑

  1. execute() 提交:裸 Runnable 未捕获异常 → 线程直接消亡,触发线程重建,引发性能抖动
  2. submit() 提交:异常被封装进 Future → 线程不会死亡,异常必须调用 get/join 才会抛出
  3. 兜底方案:可通过 Thread.setUncaughtExceptionHandler() 或线程池工厂统一配置未捕获异常处理器;也可重写线程池的 afterExecute() 扩展方法,实现任务异常的统一兜底处理。

6.7 Lambda/Stream 受检异常 全套生产工具类

// 函数式接口 - Function
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Exception> {
    R apply(T t) throws E;
}

// 函数式接口 - Consumer
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
    void accept(T t) throws E;
}

// 函数式接口 - Runnable
@FunctionalInterface
public interface ThrowingRunnable<E extends Exception> {
    void run() throws E;
}

// 统一包装工具
public class ExceptionWrapUtil {
    public static <T, R> Function<T, R> wrapFunc(ThrowingFunction<T, R, Exception> f) {
        return t -> {
            try { return f.apply(t); }
            catch (Exception e) { throw new BusinessException("执行异常", e); }
        };
    }

    public static <T> Consumer<T> wrapConsumer(ThrowingConsumer<T, Exception> c) {
        return t -> {
            try { c.accept(t); }
            catch (Exception e) { throw new BusinessException("执行异常", e); }
        };
    }
}

7. 自定义异常生产规范(可直接复用)

7.1 继承原则

  1. 可预知业务异常(余额不足、参数错误):继承RuntimeException
  2. 系统未知致命异常:单独定义SystemException
  3. 绝大多数业务场景不建议自定义受检异常;仅底层框架、SDK在真正可恢复场景可谨慎使用

7.2 序列化严谨说明

  1. JDK原生序列化:依赖 serialVersionUID 做版本校验,无强制无参构造要求,但建议保留保证最大兼容性;
  2. Hessian、Dubbo Hessian2 等RPC框架:不依赖Java原生序列化机制,不需要 serialVersionUID,但通常要求提供无参构造以保证反序列化成功。
  3. 分布式场景坑:
    • 需确保消费端存在对应自定义异常类,否则反序列化会失败并退化为 ClassNotFoundException,丢失原始异常信息;
    • Throwable完整堆栈序列化后体积较大,跨服务传输会占用大量带宽,分布式架构中通常对异常做精简序列化,仅传递错误码、消息和根因类名,不传输完整堆栈;
    • JDK原生序列化中,StackTraceElement在不同JDK版本间存在兼容性风险,跨版本RPC调用可能出现反序列化异常。

7.3 生产级完整模板

public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    private final String errorCode;

    // 通用异常构造:消息 + 原因
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
        this.errorCode = null;
    }

    // 业务异常构造:错误码 + 消息
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    // 业务异常构造:错误码 + 消息 + 原因
    public BusinessException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public BusinessException(String errorCode, Throwable cause) {
        super(cause);
        this.errorCode = errorCode;
    }

    public BusinessException(String errorCode) {
        this.errorCode = errorCode;
    }

    // 无参构造:兼容RPC框架反序列化
    public BusinessException() {
        super();
        this.errorCode = null;
    }

    public String getErrorCode() { return errorCode; }
}

7.4 规范细则

  1. 类名统一以 Exception 结尾;
  2. 错误信息携带订单号、用户ID、请求参数等上下文,严格屏蔽密码、Token等敏感数据;
  3. 禁止在 finally、close 方法中空 catch 吞异常且不打印日志;
  4. 错误码与异常消息分离,不要在 message 中重复拼接错误码,由全局异常处理器统一格式化。

8. 高级硬核场景:OOME+并发异常+虚拟线程

8.1 五种OOM精准诊断(生产排障神器)

错误日志内存区域根因分析
Java heap space堆内存内存泄漏、大对象、堆参数过小
GC overhead limit exceeded堆内存GC耗时98%+、回收效率极低,不一定是物理内存耗尽
Metaspace元空间动态生成类过多(CGLIB/反射)
Direct buffer memory堆外直接内存NIO ByteBuffer未释放
Unable to create new native thread系统线程限制线程数超限、虚拟内存不足、-Xss栈大小过大

⚠️重要说明:

  1. OutOfMemoryError 即使被捕获,绝大多数场景也无法正常恢复(堆耗尽后无多余内存支撑后续逻辑),顶层捕获仅用于记录临终日志、触发优雅降级,不可用于正常业务流程。
  2. 捕获OOM后,尽量避免创建新对象、拼接字符串,否则极易触发二次OOM导致日志记录失败;最佳实践是预分配固定日志消息,仅输出预设文本和已有异常对象。

8.2 InterruptedException 最佳实践

  1. sleep/wait/join阻塞被中断时抛出;JVM会自动清空中断标记
  2. 最佳实践:不清楚上层业务逻辑时,务必恢复中断标记 Thread.currentThread().interrupt()
  3. 若当前层为线程终止终点、无需上层感知,可终止任务、记录日志,不恢复标记。

补充:Thread.stop() 通过抛出ThreadDeath异常强制终止线程,该方法已被废弃,会破坏对象状态一致性,生产环境严禁使用。

8.3 JDK新特性补充

  1. JDK14 Helpful NPE:精准提示空指针对象与调用链,极大降低排障成本
  2. JDK9+ StackWalker:低开销的通用栈遍历API,用于替代 Thread.getStackTrace()、new Throwable().getStackTrace() 等传统方式,适合采样监控、轻量日志场景,并非替代异常创建时的 fillInStackTrace 内置流程
  3. JDK21虚拟线程:虚拟线程默认是守护线程,未捕获异常不会阻止JVM退出;默认仍会通过Thread.dispatchUncaughtException将异常堆栈输出到标准错误流(System.err),虚拟线程本身未修改该机制。生产环境因标准错误流通常未被日志系统收集,导致异常不可见,建议显式配置 UncaughtExceptionHandler 统一处理。
  4. JDK21结构化并发:StructuredTaskScope 中多个子任务异常会被聚合管理,支持“任一失败则全部取消”的失败策略,异常传播机制与独立线程不同,属于并发编程的进阶扩展。

9. 生产最佳实践 & 避坑指南

  1. 业务代码禁止捕获Throwable/Error;仅顶层线程、定时任务、框架监控可兜底捕获,用于防止线程消亡、记录临终日志。
  2. 严禁空catch吞异常:无日志、无处理的空捕获是生产排障噩梦。
  3. 优先捕获具体异常:业务代码禁止无脑 catch(Exception);仅网关、定时任务、线程池顶层可做兜底捕获。
  4. 禁止用异常做业务流程控制:异常创建开销高,高频判断用状态码、布尔值或空对象。
  5. 资源关闭优先使用TWR:彻底规避 finally 异常覆盖主异常、资源泄漏的问题。
  6. 日志打印规范
log.error("操作失败", e);                // ✅ 标准写法,完整打印堆栈
log.error("操作失败:{}", "订单号", e);    // ✅ 最后一个参数为Throwable,正常打印堆栈
log.error("操作失败:" + e);               // ❌ 字符串拼接,仅调用toString,丢失堆栈
log.error("操作失败:{}", e.getMessage()); // ❌ 仅打印消息,丢失堆栈与cause
log.error("操作失败:{}", e.toString());   // ❌ 仅打印字符串,丢失完整堆栈

说明:SLF4J 规范中,当方法最后一个参数为 Throwable 类型时,会被识别为日志异常并完整打印堆栈,不会被当作普通占位符参数。

  1. 彻底禁用 e.printStackTrace():输出到控制台、不进入业务日志,高并发下还会带来性能问题。

  2. 禁止 close/finally 内静默吞异常:资源关闭失败必须记录日志。

  3. Spring异常边界

    • Filter 属于Servlet容器层面,在DispatcherServlet之前执行,其异常无法被 @ControllerAdvice 捕获;
    • HandlerInterceptor 是Spring MVC内置组件,其方法抛出的异常会进入DispatcherServlet异常处理流程,可被全局异常处理器正常捕获;
    • @Async void 返回方法由 AsyncUncaughtExceptionHandler 处理;
    • @Async 返回 CompletableFuture 时,全局异步异常处理器不生效——因为方法正常返回了CompletableFuture对象,异常被封装在Future内部未抛到方法外,因此不会触发处理器;
    • 事务回滚联动坑:手动catch吞掉异常后,Spring事务管理器无法感知异常,会导致事务不回滚;解决方案是手动设置回滚标记TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),或在catch中重新抛出异常。
    • 定时任务异常:Spring @Scheduled、XXL-Job等定时任务抛出的异常,不会被@ControllerAdvice全局异常处理器捕获,需单独配置兜底异常处理。
  4. 全局统一异常处理:业务系统统一用全局异常处理器,杜绝 Controller 层遍地 try-catch。对外接口需脱敏异常信息,避免泄露服务器堆栈、组件版本等敏感内容;对内接口可保留完整堆栈便于排障。

  5. 异常监控要点:生产环境需监控异常QPS、异常类型占比、TopN高频异常堆栈,快速发现线上服务故障。

10. 面试高频易错速记表(终极背诵版)

核心知识点关键易错点
ClassNotFoundException vs NoClassDefFoundError主动加载失败VS运行时链接缺失,受检异常VS Error
finally return/throw覆盖返回值与异常;return会直接吞掉原异常
finally修改返回值基本类型无效;引用类型可改堆内对象属性,引用地址不变
finally不执行场景System.exit、halt、线程终止、系统崩溃
TWR执行顺序try执行→资源关闭(隐式finally)→catch捕获→显式finally执行
TWR异常抑制try异常为主异常,close异常被suppressed;仅close异常则第一个为主异常
TWR资源初始化后续资源初始化失败,前面已成功资源会自动关闭
TWR null资源关闭不报NPE,try内使用会报NPE
multi-catch无父子关系、隐式final,否则编译报错
方法重写throws子类不能拓宽受检异常范围;子类构造器只能throws不能内部捕获父构造异常
异常性能开销主要来自fillInStackTrace,无异常时几乎零开销
Lambda受检异常标准Stream接口不支持透传,需手动包装
InterruptedException不确定上层逻辑务必恢复中断标记
whenComplete异常不修改上游异常;自身抛异常会直接丢弃原结果/原异常
CompletableFuture get/joinget为受检异常、join为非受检异常
initCause仅可调用一次,构造器已传cause不可重复调用
线程池异常execute会消亡线程 / submit封装进Future不消亡
Spring全局异常Filter异常捕获不到,Interceptor异常可以捕获
事务与异常手动吞异常会导致事务不回滚,需手动标记回滚

💡一句话终极总结:
异常是Java的错误契约,受检异常代表可恢复业务问题,运行时异常代表代码逻辑Bug,Error代表系统致命故障;善用TWR、异常链、全局处理,是高级开发者的工程化核心素养。
在这里插入图片描述


更多推荐