欢迎光临
我们一直在努力

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

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、SQLException NPE、IllegalArgumentException
设计原则 调用方必须面对并处理 通过代码逻辑规避,无需强制捕获

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

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

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

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

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

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

    2.4 现代框架的设计趋势

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

  • 受检异常层层传递,污染方法签名,代码极度臃肿;
  • 标准函数式接口不支持 throws,无法兼容函数式编程;
  • 业务中绝大多数异常无法手动恢复,全局统一处理更优雅。
  • ✨Effective Java 权威完整语义:
    可恢复、需要调用方主动处理的异常 → 用受检异常
    程序Bug、无法恢复的异常 → 用运行时异常
    现代框架摒弃大量受检异常是工程取舍,并非完全否定受检异常设计。

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

  • 调用链污染:每层方法都需追加throws,破坏代码整洁性;
  • 与函数式编程不兼容,Stream场景无法直接抛出;
  • 开发者普遍粗暴 catch(Exception){} 吞异常,违背设计初衷。
  • 客观补充:受检异常是Java的特色设计,初衷是强制开发者重视并处理可恢复的外部错误;现代工程的“去受检异常化”是业务场景下的权衡,并非设计本身的失败。

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

    3.1 核心字节码指令

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

    3.2 字节码异常表(Exception Table)

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

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

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

    3.3 异常栈展开完整流程

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

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

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

  • 正常/return出口:在所有正常退出路径、return 语句前复制 finally 代码块;
  • 异常出口:通过异常表中 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编译优化严谨说明

  • JIT 会将 catch 块标记为冷路径,不影响热路径的指令优化;频繁抛出异常会影响分支预测效果。
  • 去优化(Deoptimization) 是JIT编译态退回解释态的机制,与 fillInStackTrace 堆栈填充是两个独立概念,无直接因果关系。
  • 通常情况下JIT不会删除athrow指令、不会随意消除异常捕获语义;特殊情况:如果JIT通过数据流分析能证明try块内绝对不会抛出对应类型的异常,会将对应catch块作为死代码消除,同时移除异常表中的对应条目。
  • 空 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 经典硬核坑与执行边界(面试必考)

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

  • 子类重写方法不能抛出更宽泛的受检异常;
  • 子类可抛空、或抛出父类异常的子类;
  • 非受检异常无约束,但不推荐滥用;
  • 子类构造器必须通过 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规范)

  • try 代码块执行
  • 在编译器生成的隐式finally块中自动逆序关闭所有资源;若 try 块已抛出主异常且 close() 也抛出异常,则 close 异常会被加入主异常的 suppressed 数组
  • 进入 catch 块捕获异常
  • 最后执行显式 finally 块
  • 依据JLS 14.20.3扩展规范:带catch/finally的TWR会被编译为嵌套结构,资源关闭在内层隐式finally中执行,外层catch捕获异常,外层finally最后执行。

    5.4 边界细节与核心优势

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

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

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

    6.4 CompletableFuture 异常终极严谨语义

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

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

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

  • get():抛出受检异常(ExecutionException、InterruptedException),必须显式 try-catch
  • join():抛出非受检异常(正常异常为 CompletionException,任务取消时抛 CancellationException),无需强制捕获
  • 6.6 线程池异常核心坑

  • execute() 提交:裸 Runnable 未捕获异常 → 线程直接消亡,触发线程重建,引发性能抖动
  • submit() 提交:异常被封装进 Future → 线程不会死亡,异常必须调用 get/join 才会抛出
  • 兜底方案:可通过 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 继承原则

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

  • JDK原生序列化:依赖 serialVersionUID 做版本校验,无强制无参构造要求,但建议保留保证最大兼容性;
  • Hessian、Dubbo Hessian2 等RPC框架:不依赖Java原生序列化机制,不需要 serialVersionUID,但通常要求提供无参构造以保证反序列化成功。
  • 分布式场景坑:
    • 需确保消费端存在对应自定义异常类,否则反序列化会失败并退化为 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 规范细则

  • 类名统一以 Exception 结尾;
  • 错误信息携带订单号、用户ID、请求参数等上下文,严格屏蔽密码、Token等敏感数据;
  • 禁止在 finally、close 方法中空 catch 吞异常且不打印日志;
  • 错误码与异常消息分离,不要在 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栈大小过大

    ⚠️重要说明:

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

  • sleep/wait/join阻塞被中断时抛出;JVM会自动清空中断标记
  • 最佳实践:不清楚上层业务逻辑时,务必恢复中断标记 Thread.currentThread().interrupt()
  • 若当前层为线程终止终点、无需上层感知,可终止任务、记录日志,不恢复标记。
  • 补充:Thread.stop() 通过抛出ThreadDeath异常强制终止线程,该方法已被废弃,会破坏对象状态一致性,生产环境严禁使用。

    8.3 JDK新特性补充

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

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

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

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

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

  • 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全局异常处理器捕获,需单独配置兜底异常处理。
  • 全局统一异常处理:业务系统统一用全局异常处理器,杜绝 Controller 层遍地 try-catch。对外接口需脱敏异常信息,避免泄露服务器堆栈、组件版本等敏感内容;对内接口可保留完整堆栈便于排障。

  • 异常监控要点:生产环境需监控异常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/join get为受检异常、join为非受检异常
    initCause 仅可调用一次,构造器已传cause不可重复调用
    线程池异常 execute会消亡线程 / submit封装进Future不消亡
    Spring全局异常 Filter异常捕获不到,Interceptor异常可以捕获
    事务与异常 手动吞异常会导致事务不回滚,需手动标记回滚

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


    赞(0)
    未经允许不得转载:171主机测评 » Java异常体系万字笔记|字节码、性能、Lambda、CompletableFuture全梳理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址