网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

Exception ​

字数: 0 字 时长: 0 分钟

1. 异常处理全面解析 ​

1.1 什么是异常 ​

异常(Exception)可以理解为:

程序运行过程中出现的非正常情况,它会改变程序原本的正常执行流程。

常见的异常场景包括:

text
文件不存在

网络连接失败

数据库访问失败

数组越界

空指针

类型转换错误

参数非法

除数为 0

1.1.1 异常对程序执行流程的影响 ​

例如:

java
public class Demo {

    public static void main(String[] args) {

        System.out.println(
                "开始执行"
        );

        System.out.println(
                10 / 0
        );

        System.out.println(
                "执行结束"
        );
    }
}

执行:

java
10 / 0

会抛出:

text
Exception in thread "main"
java.lang.ArithmeticException: / by zero

执行过程:

text
开始执行
    ↓
10 / 0
    ↓
ArithmeticException
    ↓
正常执行流程被打断

因此后面的:

java
System.out.println(
        "执行结束"
);

不会继续执行。

如果异常一直没有被处理:

text
异常发生
    ↓
当前正常执行流程被打断
    ↓
异常沿调用栈向上传播
    ↓
如果最终仍然没人处理
    ↓
当前线程终止

如果使用异常处理:

java
public class Demo {

    public static void main(String[] args) {

        try {

            System.out.println(
                    10 / 0
            );

        } catch (
                ArithmeticException e) {

            System.out.println(
                    "除数不能为 0"
            );
        }

        System.out.println(
                "程序继续执行"
        );
    }
}

输出:

text
除数不能为 0
程序继续执行

所以异常处理并不是:

text
异常发生以后什么都没发生

而是:

异常发生以后,由程序决定应该如何处理这次异常。

Java 的异常处理机制可以帮助我们:

text
捕获异常

记录异常

进行补偿

重试操作

释放资源

继续向上抛出

转换成业务异常

1.1.2 Java 异常体系 ​

Java 异常体系的根类是:

java
java.lang.Throwable

整体结构可以简单理解:

text
Throwable
├── Error
│
└── Exception
    ├── RuntimeException
    │   ├── NullPointerException
    │   ├── ArithmeticException
    │   ├── ClassCastException
    │   ├── IndexOutOfBoundsException
    │   └── ...
    │
    └── 其他 Exception
        ├── IOException
        ├── SQLException
        ├── ClassNotFoundException
        └── ...

只有:

java
Throwable

以及它的子类对象才能:

text
被 throw 抛出

被 catch 捕获

例如:

java
throw new RuntimeException(
        "发生异常"
);

1.1.3 异常信息与异常堆栈 ​

异常发生以后,通常会看到类似:

text
java.lang.ArithmeticException: / by zero
    at Demo.test(Demo.java:20)
    at Demo.main(Demo.java:10)

这里包含:

text
异常类型

异常信息

异常发生位置

方法调用链

这些信息统称为:

text
异常堆栈
Stack Trace

对于普通用户来说,通常不应该直接看到:

text
NullPointerException

SQLException

IOException

这样的底层异常。

而对于开发人员来说:

异常类型、异常消息和异常堆栈都是定位问题的重要信息,不应该随意丢失。

开发阶段可以:

java
e.printStackTrace();

打印堆栈。

实际项目中通常更推荐使用日志框架:

java
log.error(
        "业务处理失败",
        e
);

这样能够保留:

text
异常类型

异常消息

完整调用栈

时间

线程

服务实例

等信息。

1.2 Exception 和 Error 的区别 ​

Throwable 主要分为:

text
Error

Exception

虽然两者都属于异常体系,但含义和处理方式并不一样。

1.2.1 Error ​

Error 通常表示:

JVM 或运行环境发生了比较严重的问题。

常见的:

text
OutOfMemoryError

StackOverflowError

NoClassDefFoundError

例如:

text
OutOfMemoryError
    ↓
JVM 无法获得足够内存

这种问题通常不是普通业务代码通过:

java
try-catch

就能够真正解决的。

所以:

Error 一般不作为正常业务异常处理机制的一部分。

1.2.2 Exception ​

Exception 表示程序运行过程中可能出现的异常情况。

例如:

text
IOException

SQLException

ClassNotFoundException

NullPointerException

ArithmeticException

其中:

text
IOException

SQLException

ClassNotFoundException

通常属于:

text
Checked Exception

而:

text
NullPointerException

ArithmeticException

ClassCastException

属于:

text
RuntimeException
    ↓
Unchecked Exception

1.2.3 Exception 和 Error 对比 ​

可以简单理解:

text
Throwable
├── Error
│   ↓
│   JVM / 系统层面的严重问题
│
└── Exception
    ↓
    程序运行过程中需要考虑的异常情况

不过不能简单认为:

text
Error
    ↓
百分之百无法处理


Exception
    ↓
百分之百可以恢复

是否能够恢复,最终还是取决于具体场景。

1.3 Checked 和 Unchecked 异常 ​

Java 异常中一个非常重要的分类就是:

text
Checked Exception

Unchecked Exception

image-20250323180058113

1.3.1 Checked Exception ​

Checked Exception 通常翻译为:

text
检查型异常

受检异常

最大的特点是:

编译器要求必须处理。

处理方式只有两种:

text
try-catch 捕获

或者

throws 声明抛出

否则无法通过编译。

常见的 Checked Exception:

text
IOException

SQLException

ClassNotFoundException

例如:

java
Class.forName(
        "com.kerwim.Demo.Demo1"
);

Class.forName() 可能抛出:

java
ClassNotFoundException

如果完全不处理,IDE 会提示:

image-20250323180330872

可以使用:

java
try {

    Class<?> clazz =
            Class.forName(
                    "com.kerwim.Demo.Demo1"
            );

} catch (
        ClassNotFoundException e) {

    e.printStackTrace();
}

也可以声明:

java
public static void main(
        String[] args)
        throws ClassNotFoundException {

    Class<?> clazz =
            Class.forName(
                    "com.kerwim.Demo.Demo1"
            );
}

所以可以简单理解:

text
Checked Exception
    ↓
编译器强制你考虑
    ↓
catch
或者
throws

1.3.2 Unchecked Exception ​

Unchecked Exception 指:

编译器不强制要求显式捕获或者声明的异常。

RuntimeException 以及它的子类都是典型的 Unchecked Exception。

例如:

text
NullPointerException

ArithmeticException

ClassCastException

ArrayIndexOutOfBoundsException

IllegalArgumentException

例如:

java
int result =
        10 / 0;

编译器允许通过。

但是运行时会抛出:

text
ArithmeticException

再例如:

java
String name = null;

System.out.println(
        name.length()
);

编译器同样不会要求:

text
try-catch

但运行时可能抛出:

text
NullPointerException

所以:

text
Unchecked Exception
    ↓
编译器不强制处理
    ↓
很多时候应该通过正确编码提前避免

1.3.3 Checked 和 Unchecked 的区别 ​

对比Checked ExceptionUnchecked Exception
编译器是否强制处理是否
是否必须 catch 或 throws是否
常见类型IOException、SQLExceptionNullPointerException、IllegalArgumentException
常见来源IO、数据库、外部资源等参数错误、逻辑错误、代码缺陷等
类型关系Exception 中除 RuntimeException 体系外RuntimeException 及其子类

不能简单理解成:

text
Checked
    ↓
一定可以恢复


Unchecked
    ↓
一定是程序员写错了

异常是否可以恢复,需要根据具体业务判断。

另外,从整个:

java
Throwable

体系来看:

java
Error

同样不要求编译器强制捕获。

不过日常讨论:

text
Checked / Unchecked Exception

时,重点通常是 Exception 体系内部的分类。

1.3.4 NoClassDefFoundError 和 ClassNotFoundException ​

这两个名字非常相似:

text
NoClassDefFoundError

ClassNotFoundException

但属于完全不同的体系。

ClassNotFoundException:

text
Exception
    ↓
Checked Exception

典型场景:

java
Class.forName(
        "com.kerwim.User"
);

程序主动动态加载某个类,但是没有找到。

常见原因:

text
类名写错

依赖不存在

目标 Class 不在 ClassPath

NoClassDefFoundError:

text
Error

通常意味着:

JVM 在运行过程中需要某个本应该存在的类,但此时无法正常获得它。

例如:

text
编译时依赖存在

运行时 jar 包缺失

另外,某个类初始化失败以后,后续再次使用这个类时,也可能出现:

text
NoClassDefFoundError

可以简单区分:

text
ClassNotFoundException
    ↓
主动加载类的时候没找到


NoClassDefFoundError
    ↓
JVM 运行过程中需要某个类
    ↓
却无法正常获得

1.4 throw 和 throws ​

Java 中有两个非常容易混淆的关键字:

text
throw

throws

虽然只差一个:

text
s

但作用完全不同。

text
throw
    ↓
真正抛出异常对象


throws
    ↓
声明方法可能向外抛出异常

1.4.1 throw ​

throw 用于:

主动抛出一个异常对象。

基本语法:

java
throw new 异常类型(
        "异常信息"
);

例如:

java
throw new IllegalArgumentException(
        "参数不能为空"
);

再例如:

java
public class ThrowDemo {

    public static void checkEligibility(
            int age) {

        if (age < 18) {

            throw new IllegalArgumentException(
                    "年龄未满 18 岁,禁止观影"
            );
        }

        System.out.println(
                "请认真观影"
        );
    }

    public static void main(
            String[] args) {

        checkEligibility(10);

        System.out.println(
                "愉快的周末"
        );
    }
}

当:

text
age < 18

时,会主动执行:

java
throw new IllegalArgumentException(...)

如果没有被捕获,后面的:

java
System.out.println(
        "愉快的周末"
);

不会继续正常执行。

image-20250323181505651

1.4.2 throws ​

throws 用于:

text
方法声明

例如:

java
public void method()
        throws IOException {
}

表示:

这个方法执行过程中可能把指定异常继续抛给调用者处理。

例如:

java
public static void loadClass()
        throws ClassNotFoundException {

    Class.forName(
            "com.kerwim.Demo.Demo1"
    );
}

此时:

text
loadClass()
    ↓
自己不处理
    ↓
把异常继续交给调用者

调用方可以:

java
try {

    loadClass();

} catch (
        ClassNotFoundException e) {

    e.printStackTrace();
}

对于 Checked Exception,如果既没有:

text
catch

也没有:

text
throws

编译器会直接报错。

image-20250323181620921

1.4.3 throws 声明多个异常 ​

throws 可以同时声明多个异常。

例如:

java
public void execute()
        throws IOException,
               SQLException {
}

表示:

text
execute()
    ↓
可能向调用者传播 IOException
    ↓
也可能传播 SQLException

多个异常之间使用:

text
,

分隔。

1.4.4 throw 和 throws 的区别 ​

对比throwthrows
作用真正抛出异常声明可能抛出的异常
使用位置方法体内部方法签名
后面跟什么异常对象异常类型
数量一次抛出一个对象可以声明多个异常
示例throw new RuntimeException()throws IOException

例如:

java
throw new ArithmeticException(
        "算术异常"
);

而:

java
public void test()
        throws ArithmeticException {
}

可以简单记成:

text
throw
    ↓
扔异常


throws
    ↓
声明我可能往外扔

1.4.5 什么时候 catch,什么时候 throws ​

没有绝对规则。

最重要的判断是:

当前这一层有没有能力正确处理这个异常。

如果当前方法能够真正进行:

text
重试

降级

补偿

返回默认值

转换业务结果

可以考虑:

java
catch

如果当前层不知道应该如何处理:

text
不知道怎么恢复
    ↓
继续向上传播

通常更合理。

不要为了消除编译报错而写:

java
try {

    // ...

} catch (
        Exception e) {

}

然后什么都不做。

这叫:

text
吞异常

会导致:

text
上层不知道失败

日志没有记录

真正的问题被隐藏

排查非常困难

1.5 try-catch-finally ​

Java 最核心的异常处理结构就是:

text
try

catch

finally

基本结构:

java
try {

    // 可能发生异常的代码

} catch (
        Exception e) {

    // 异常处理

} finally {

    // 清理逻辑
}

1.5.1 try ​

try 用来包裹:

可能发生异常的代码。

例如:

java
try {

    System.out.println(
            "第一行"
    );

    int result =
            10 / 0;

    System.out.println(
            "第三行"
    );

} catch (
        ArithmeticException e) {

    System.out.println(
            "发生算术异常"
    );
}

输出:

text
第一行
发生算术异常

发生异常以后:

java
System.out.println(
        "第三行"
);

不会继续正常执行。

执行流程:

text
进入 try
    ↓
正常执行代码
    ↓
发生异常
    ↓
try 后续代码停止
    ↓
寻找匹配的 catch

1.5.2 catch ​

catch 用来:

捕获并处理 try 中抛出的异常。

例如:

java
try {

    int result =
            62 / 0;

} catch (
        ArithmeticException e) {

    System.out.println(
            "除数不能为零"
    );
}

这里:

java
ArithmeticException e

表示:

text
捕获 ArithmeticException
    ↓
异常对象保存到变量 e

之后可以使用:

java
e.getMessage();

e.getCause();

e.printStackTrace();

等方法获取异常信息。

1.5.3 多个 catch ​

一个:

java
try

后面可以跟多个:

java
catch

例如:

java
public static void test() {

    try {

        int[] array =
                new int[8];

        array[4] =
                30 / 0;

        System.out.println(
                "try 块最后一行"
        );

    } catch (
            ArithmeticException e) {

        System.out.println(
                "除数不能为零"
        );

    } catch (
            ArrayIndexOutOfBoundsException e) {

        System.out.println(
                "数组越界"
        );

    } catch (
            Exception e) {

        System.out.println(
                "其他异常"
        );
    }

    System.out.println(
            "try-catch 之后"
    );
}

这里:

java
30 / 0

首先抛出:

text
ArithmeticException

所以进入:

java
catch (
        ArithmeticException e)

如果改成:

java
array[9] =
        30 / 1;

就会抛出:

text
ArrayIndexOutOfBoundsException

然后进入第二个:

java
catch

1.5.4 catch 的顺序 ​

如果异常之间存在继承关系:

text
Exception
    ↑
ArithmeticException

那么:

更具体的子类异常必须写在前面。

正确:

java
try {

    // ...

} catch (
        ArithmeticException e) {

    // ...

} catch (
        Exception e) {

    // ...
}

不能反过来:

java
try {

    // ...

// 编译错误
} catch (
        Exception e) {

} catch (
        ArithmeticException e) {

}

因为:

java
Exception

已经能够捕获:

java
ArithmeticException

那么后面的:

java
catch (
        ArithmeticException e)

永远不会有机会执行。

所以:

text
多个 catch
    ↓
子类异常在前
    ↓
父类异常在后

1.5.5 Multi-Catch ​

如果多个异常的处理逻辑完全一样,可以使用:

text
|

同时捕获。

例如:

java
try {

    int[] array =
            new int[8];

    array[9] =
            30 / 1;

} catch (
        ArithmeticException
        | ArrayIndexOutOfBoundsException e) {

    System.out.println(
            "发生计算或数组异常"
    );
}

这种语法称为:

text
Multi-Catch

相比:

java
catch (
        ArithmeticException e) {

    // 相同逻辑
}

catch (
        ArrayIndexOutOfBoundsException e) {

    // 相同逻辑
}

可以减少重复代码。

1.5.6 finally ​

finally 通常用于:

无论正常执行还是发生异常,都希望执行的清理逻辑。

基本结构:

java
try {

    // 业务代码

} catch (
        Exception e) {

    // 异常处理

} finally {

    // 清理逻辑
}

例如:

java
try {

    System.out.println(
            "执行业务"
    );

} finally {

    System.out.println(
            "执行清理"
    );
}

通常情况下:

text
是否发生异常
    ↓
finally 都会执行

finally 常用于:

text
释放资源

清理临时状态

关闭连接

执行收尾操作

1.5.7 finally 与 return ​

例如:

java
public static int test() {

    try {

        return 1;

    } finally {

        System.out.println(
                "finally"
        );
    }
}

虽然:

java
try

中已经执行:

java
return 1;

但在方法真正返回之前,仍然会执行:

java
finally

类似地:

text
break

continue

通常也不会跳过 finally。

不过不要在:

java
finally

中写:

java
return

例如:

java
try {

    return 1;

} finally {

    return 2;
}

因为它可能:

text
覆盖原来的返回值

吞掉原来的异常

让控制流程非常难理解

所以:

finally 负责清理,不要负责改变程序的正常返回逻辑。

1.5.8 finally 不一定百分之百执行 ​

平时可以理解:

text
finally 基本都会执行

但它并不是物理意义上的:

text
100% 一定执行

例如:

java
System.exit(0);

会请求 JVM 直接结束。

这时:

java
finally

可能没有机会执行。

image-20250323182714071

另外:

text
JVM 崩溃

进程被强制杀死

kill -9

机器断电

同样无法保证:

text
finally

被执行。

所以更准确的说法是:

在 JVM 正常执行 Java 控制流程的情况下,finally 具有非常强的执行保证。

1.5.9 finally 与资源释放 ​

过去:

text
finally

经常用于关闭:

text
IO 流

Socket

数据库连接

例如:

java
OutputStream output = null;

try {

    output =
            new FileOutputStream(
                    "filename"
            );

    // 写入数据

} finally {

    if (output != null) {

        output.close();
    }
}

这样可以尽量保证:

text
正常执行
或者
发生异常
    ↓
资源最终都能够关闭

现代 Java 对实现了:

java
AutoCloseable

的资源,一般更推荐:

text
try-with-resources

例如:

java
try (
        OutputStream output =
                new FileOutputStream(
                        "filename"
                )
) {

    // 使用资源
}

资源会自动关闭。

这部分后面在:

text
IO

中再详细说明。

1.6 小结 ​

1.6.1 Java 异常体系 ​

Java 异常体系的根类:

text
Throwable

主要分成:

text
Throwable
├── Error
└── Exception

其中:

text
Error
    ↓
通常代表 JVM / 运行环境严重问题

而:

text
Exception
    ↓
程序运行过程中需要处理的异常情况

1.6.2 Checked 和 Unchecked ​

text
Checked Exception
    ↓
编译器要求处理
    ↓
catch 或 throws

而:

text
RuntimeException
    ↓
Unchecked Exception
    ↓
编译器不强制处理

1.6.3 异常处理核心关键字 ​

text
try
    ↓
可能发生异常的代码


catch
    ↓
捕获并处理异常


finally
    ↓
执行清理逻辑


throw
    ↓
主动抛出异常对象


throws
    ↓
声明方法可能向外传播异常

1.6.4 异常传播流程 ​

text
异常发生
    ↓
当前正常流程被打断
    ↓
寻找匹配的 catch
    ↓
找到
    ↓
执行异常处理


没有找到
    ↓
继续向调用者传播
    ↓
一直传播到线程顶层
    ↓
仍然没人处理
    ↓
当前线程终止

1.6.5 实际开发注意事项 ​

实际开发中需要记住:

text
能真正处理再 catch

不能处理就继续抛

不要吞异常

不要丢失原始异常

日志要保留完整异常对象

更具体的 catch 放前面

能够提前校验的问题不要依赖异常控制流程

资源一定要正确释放

例如转换异常时:

java
catch (
        SQLException e) {

    throw new RuntimeException(
            "数据库操作失败",
            e
    );
}

这里把:

java
e

作为:

text
cause

继续传递。

于是:

text
新的 RuntimeException
    ↓
仍然保留原始 SQLException
    ↓
完整异常调用链不会丢失

生产环境记录日志时,也更推荐:

java
log.error(
        "保存用户失败",
        e
);

而不是:

java
log.error(
        e.getMessage()
);

因为后者通常只有:

text
异常消息

而没有完整:

text
Stack Trace

一句话概括:

Java 异常机制的本质,就是让程序在出现非正常情况时,能够有结构地传播、定位和处理问题。

2. try-with-resources ​

try-with-resources 是 Java 7 引入的一种资源管理语法,主要用于自动关闭:

text
IO 流

Reader / Writer

Socket

数据库连接

以及其他需要手动释放的资源

它解决的核心问题就是:

资源使用完成后,由 Java 自动调用 close() 方法,不再需要手动在 finally 中关闭。

使用 try-with-resources 的前提是:

text
资源类型实现了 AutoCloseable

或者它的子接口:

java
Closeable

整体结构:

java
try (
        // 需要自动关闭的资源
) {

    // 使用资源

} catch (
        Exception e) {

    // 异常处理
}

2.1 为什么需要 try-with-resources ​

在 Java 7 之前,通常使用:

text
try-catch-finally

关闭资源。

例如读取文件:

java
public class TryCatchFinallyDecoder {

    public static void main(
            String[] args) {

        BufferedReader reader = null;

        try {

            String path =
                    TryCatchFinallyDecoder.class
                            .getResource(
                                    "/牛逼.txt"
                            )
                            .getFile();

            String decodePath =
                    URLDecoder.decode(
                            path,
                            "UTF-8"
                    );

            reader =
                    new BufferedReader(
                            new FileReader(
                                    decodePath
                            )
                    );

            String line;

            while (
                    (line =
                            reader.readLine())
                            != null) {

                System.out.println(
                        line
                );
            }

        } catch (
                IOException e) {

            e.printStackTrace();

        } finally {

            if (reader != null) {

                try {

                    reader.close();

                } catch (
                        IOException e) {

                    e.printStackTrace();
                }
            }
        }
    }
}

这里真正读取文件的代码其实并不复杂:

java
while (
        (line = reader.readLine())
                != null) {

    System.out.println(line);
}

但为了关闭:

java
BufferedReader

却需要写大量:

text
finally

null 判断

try-catch

close()

整个结构变成:

text
创建资源
    ↓
try 使用
    ↓
catch 处理异常
    ↓
finally
    ↓
判断资源是否为 null
    ↓
再次 try-catch
    ↓
close()

代码比较臃肿。

2.2 finally 关闭资源存在的问题 ​

传统:

java
try-finally

除了代码繁琐,还有一个比较重要的问题:

业务代码和关闭资源的代码都可能抛出异常。

例如定义:

java
class MyFinallyReadLineThrow {

    public void readLine()
            throws Exception {

        throw new Exception(
                "readLine"
        );
    }

    public void close()
            throws Exception {

        throw new Exception(
                "close"
        );
    }
}

然后:

java
public static void main(
        String[] args)
        throws Exception {

    MyFinallyReadLineThrow resource =
            null;

    try {

        resource =
                new MyFinallyReadLineThrow();

        resource.readLine();

    } finally {

        resource.close();
    }
}

这里:

text
readLine()
    ↓
抛出异常


finally
    ↓
close()
    ↓
又抛出异常

最终:

text
close() 的异常

可能覆盖掉原本:

text
readLine() 的异常

image-20250323183326181

这会导致:

text
真正首先发生的问题
    ↓
被资源关闭异常掩盖

从而增加问题排查难度。

而:

text
try-with-resources

专门处理了这种情况。

2.3 try-with-resources 基本使用 ​

使用:

text
try-with-resources

后,可以直接把资源声明放到:

java
try (...)

中。

例如:

java
try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader(
                                decodePath
                        )
                )
) {

    String line;

    while (
            (line =
                    reader.readLine())
                    != null) {

        System.out.println(
                line
        );
    }

} catch (
        IOException e) {

    e.printStackTrace();
}

这里已经不需要:

text
finally

reader.close()

因为 Java 会自动调用:

java
reader.close();

整个流程变成:

text
创建资源
    ↓
进入 try
    ↓
使用资源
    ↓
离开 try
    ↓
自动 close()

无论:

text
正常执行结束

还是

try 中抛出异常

都会尝试关闭资源。

2.4 AutoCloseable 接口 ​

try-with-resources 能够自动关闭资源的关键就在:

java
AutoCloseable

接口。

它的核心结构非常简单:

java
public interface AutoCloseable {

    void close()
            throws Exception;
}

也就是说:

只要一个对象实现了 AutoCloseable,就可以放进 try-with-resources 中。

例如 Java 中大量资源类都满足这个条件:

text
BufferedReader

FileReader

InputStream

OutputStream

Connection

Statement

ResultSet

其中很多 IO 类实现的是:

java
Closeable

而:

java
Closeable

本身又继承:

java
AutoCloseable

关系可以简单理解:

text
AutoCloseable
      ↑
   Closeable
      ↑
各种 IO 资源

2.5 多个资源 ​

try-with-resources 可以同时管理多个资源。

例如:

java
try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader(
                                decodePath
                        )
                );

        PrintWriter writer =
                new PrintWriter(
                        new File(
                                writePath
                        )
                )
) {

    String line;

    while (
            (line =
                    reader.readLine())
                    != null) {

        writer.println(line);
    }

} catch (
        IOException e) {

    e.printStackTrace();
}

多个资源之间使用:

text
;

分隔。

例如:

java
try (
        ResourceA a =
                new ResourceA();

        ResourceB b =
                new ResourceB()
) {
}

2.5.1 多个资源的关闭顺序 ​

多个资源的关闭顺序和创建顺序相反。

例如:

java
try (
        ResourceA a =
                new ResourceA();

        ResourceB b =
                new ResourceB();

        ResourceC c =
                new ResourceC()
) {
}

创建顺序:

text
A
↓
B
↓
C

关闭顺序:

text
C
↓
B
↓
A

也就是:

后创建的资源先关闭。

这种设计比较符合资源之间可能存在依赖关系的情况。

2.6 自定义资源 ​

如果自己的类也需要支持:

text
try-with-resources

只需要实现:

java
AutoCloseable

即可。

例如:

java
class MyResource
        implements AutoCloseable {

    @Override
    public void close()
            throws Exception {

        System.out.println(
                "关闭自定义资源"
        );
    }
}

使用:

java
public class TryWithResourcesCustom {

    public static void main(
            String[] args) {

        try (
                MyResource resource =
                        new MyResource()
        ) {

            System.out.println(
                    "使用资源"
            );

        } catch (
                Exception e) {

            e.printStackTrace();
        }
    }
}

输出:

text
使用资源
关闭自定义资源

可以看到:

java
close()

并没有手动调用。

Java 会在离开:

java
try

作用域时自动调用。

2.7 自动调用 close() ​

再给:

java
MyResource

增加一个方法:

java
class MyResource
        implements AutoCloseable {

    public void out()
            throws Exception {

        System.out.println(
                "kerwim,Java 牛马"
        );
    }

    @Override
    public void close()
            throws Exception {

        System.out.println(
                "关闭自定义资源"
        );
    }
}

调用:

java
public static void main(
        String[] args) {

    try (
            MyResource resource =
                    new MyResource()
    ) {

        resource.out();

    } catch (
            Exception e) {

        e.printStackTrace();
    }
}

输出:

text
kerwim,Java 牛马
关闭自定义资源

整个过程:

text
new MyResource()
    ↓
resource.out()
    ↓
try 执行结束
    ↓
自动 resource.close()

所以:

try-with-resources 本质上仍然会调用资源的 close(),只是这部分工作由编译器帮我们生成。

2.8 Suppressed Exception ​

try-with-resources 一个非常重要的能力就是:

text
Suppressed Exception
被抑制异常

假设资源方法和关闭方法都会抛出异常:

java
class MyResourceOutThrow
        implements AutoCloseable {

    public void out()
            throws Exception {

        throw new Exception(
                "out()"
        );
    }

    @Override
    public void close()
            throws Exception {

        throw new Exception(
                "close()"
        );
    }
}

调用:

java
public static void main(
        String[] args) {

    try (
            MyResourceOutThrow resource =
                    new MyResourceOutThrow()
    ) {

        resource.out();

    } catch (
            Exception e) {

        e.printStackTrace();
    }
}

这里:

text
out()
    ↓
首先抛出异常


自动 close()
    ↓
再次抛出异常

try-with-resources 不会让:

text
close()

的异常覆盖:

text
out()

的异常。

而是:

text
out() 异常
    ↓
作为主要异常


close() 异常
    ↓
作为 Suppressed Exception

输出类似:

text
java.lang.Exception: out()
    at ...

Suppressed: java.lang.Exception: close()
    at ...

所以:

text
主要异常
    ↓
正常保留


资源关闭异常
    ↓
作为 Suppressed 保存

不会丢失异常信息。

2.9 addSuppressed() 和 getSuppressed() ​

Java 的:

java
Throwable

提供了:

java
addSuppressed()

用于记录被抑制的异常。

概念上类似:

java
primaryException
        .addSuppressed(
                closeException
        );

也可以通过:

java
getSuppressed()

获取这些异常。

例如:

java
catch (
        Exception e) {

    Throwable[] suppressed =
            e.getSuppressed();

    for (
            Throwable throwable :
                    suppressed
    ) {

        System.out.println(
                throwable.getMessage()
        );
    }
}

如果:

text
out()

和:

text
close()

都抛出了异常,那么:

java
e

代表主要异常。

而:

java
e.getSuppressed()

可以得到关闭资源时产生的异常。

这就是 try-with-resources 相比传统:

text
try-finally

更加可靠的重要原因之一。

2.10 编译器如何处理 try-with-resources ​

例如:

java
try (
        MyResource resource =
                new MyResource()
) {

    resource.out();
}

编译以后,Java 编译器会生成相应的资源关闭逻辑。

概念上可以理解成:

java
MyResource resource =
        new MyResource();

Throwable primaryException =
        null;

try {

    resource.out();

} catch (
        Throwable e) {

    primaryException = e;

    throw e;

} finally {

    if (resource != null) {

        if (primaryException != null) {

            try {

                resource.close();

            } catch (
                    Throwable closeException) {

                primaryException
                        .addSuppressed(
                                closeException
                        );
            }

        } else {

            resource.close();
        }
    }
}

真实生成的字节码和反编译代码可能会因:

text
JDK 版本

编译器版本

反编译工具

而有所不同。

因此不需要死记反编译后的代码。

真正要理解的是:

text
try-with-resources
    ↓
自动 close()
    ↓
保留主要异常
    ↓
关闭异常作为 Suppressed

2.11 Java 9 对 try-with-resources 的改进 ​

Java 7 中通常需要在:

java
try (...)

里面声明资源:

java
try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader(
                                "data.txt"
                        )
                )
) {

    // 使用 reader
}

Java 9 开始,如果一个变量已经在外面声明,并且属于:

text
final

或者

effectively final

就可以直接放进:

java
try (...)

中。

例如:

java
BufferedReader reader =
        new BufferedReader(
                new FileReader(
                        "data.txt"
                )
        );

try (reader) {

    System.out.println(
            reader.readLine()
    );
}

这里:

java
reader

没有被重新赋值,因此属于:

text
effectively final

可以直接使用。

相比 Java 7:

text
Java 7
    ↓
通常在 try() 中重新声明资源


Java 9+
    ↓
可以直接使用已有的 final /
effectively final 资源变量

2.12 try-with-resources 与 finally ​

try-with-resources 并不意味着:

text
finally 被废弃了

两者解决的问题不同。

try-with-resources:

text
主要用于
自动关闭资源

finally:

text
用于执行通用的收尾逻辑

例如:

java
try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader(
                                "data.txt"
                        )
                )
) {

    // 使用资源

} catch (
        IOException e) {

    // 处理异常

} finally {

    System.out.println(
            "整个读取流程结束"
    );
}

这里:

text
reader.close()
    ↓
try-with-resources 自动负责


输出收尾日志
    ↓
finally 负责

所以它们并不冲突。

2.13 使用原则 ​

对于需要关闭的资源:

text
IO 流

Socket

JDBC 资源

AutoCloseable 对象

一般优先考虑:

java
try-with-resources

而不是手动:

java
try {

} finally {

    resource.close();
}

原因主要有:

text
代码更加简洁

不容易忘记关闭资源

多个资源管理更加方便

自动按照逆序关闭

异常信息更加完整

支持 Suppressed Exception

尤其不要写成:

java
try {

    // 使用资源

} finally {

    resource.close();
}

然后完全忽略:

text
close() 本身也可能失败

的问题。

2.14 小结 ​

try-with-resources 的核心目的就是:

自动管理需要关闭的资源。

基本写法:

java
try (
        Resource resource =
                new Resource()
) {

    // 使用资源
}

使用条件:

text
资源实现 AutoCloseable

执行流程:

text
创建资源
    ↓
执行 try
    ↓
正常结束 / 发生异常
    ↓
自动调用 close()

多个资源:

text
创建顺序
A → B → C


关闭顺序
C → B → A

发生多个异常时:

text
业务异常
    ↓
作为主要异常


close() 异常
    ↓
作为 Suppressed Exception

常用方法:

java
Throwable.getSuppressed()

可以获取被抑制的异常。

Java 9 以后还可以直接使用:

text
final

或者

effectively final

的已有资源变量。

最后可以简单记成:

text
传统方式

try
    ↓
使用资源
    ↓
finally
    ↓
手动 close()


try-with-resources

try (资源)
    ↓
使用资源
    ↓
Java 自动 close()

一句话概括:

凡是实现了 AutoCloseable、使用结束后必须关闭的资源,优先考虑 try-with-resources。

3. 异常处理最佳实践 ​

异常处理不是简单地:

text
哪里报错
    ↓
哪里 try-catch

真正需要考虑的是:

text
异常是否应该发生

应该在哪一层抛出

应该在哪一层捕获

是否需要转换异常

是否需要记录日志

是否需要重试或补偿

是否需要释放资源

下面整理一些实际开发中比较重要的异常处理原则。

3.1 不要用 catch 掩盖本可以提前避免的 RuntimeException ​

对于很多可以通过正常逻辑提前判断的问题,不应该依赖:

java
try-catch

来控制流程。

例如:

java
String name = null;

不要这样:

java
try {

    System.out.println(
            name.length()
    );

} catch (
        NullPointerException e) {

    // ...
}

如果:

text
null

本身属于允许出现的状态,更合理的是提前判断:

java
if (name != null) {

    System.out.println(
            name.length()
    );
}

再例如:

java
if (index >= 0
        && index < list.size()) {

    System.out.println(
            list.get(index)
    );
}

而不是故意调用:

java
list.get(index);

然后再捕获:

text
IndexOutOfBoundsException

可以简单理解:

text
能够正常判断
    ↓
优先正常判断


真正的异常情况
    ↓
再交给异常机制

不过也不能理解成:

RuntimeException 永远不能捕获。

例如:

text
Controller 全局异常处理

任务调度入口

线程执行边界

RPC / 消息消费边界

这些边界层有时需要统一捕获:

java
RuntimeException

进行:

text
日志记录

事务回滚

统一响应

任务隔离

重点不是:

text
绝对不能 catch RuntimeException

而是:

不要把本来应该通过正常代码避免的问题,全部交给 RuntimeException 来控制流程。

3.2 优先使用 try-with-resources 关闭资源 ​

对于实现:

java
AutoCloseable

的资源,优先使用:

text
try-with-resources

例如:

java
try (
        FileInputStream inputStream =
                new FileInputStream(
                        "./tmp.txt"
                )
) {

    // 使用资源

} catch (
        IOException e) {

    log.error(
            "读取文件失败",
            e
    );
}

不要:

java
public void readFile() {

    FileInputStream inputStream =
            null;

    try {

        inputStream =
                new FileInputStream(
                        "./tmp.txt"
                );

        // 使用资源

        inputStream.close();

    } catch (
            IOException e) {

        log.error(
                "读取文件失败",
                e
        );
    }
}

问题在于:

text
资源创建成功
    ↓
业务代码发生异常
    ↓
还没执行到 close()
    ↓
资源没有关闭

try-with-resources 会自动:

text
正常执行结束
或者
发生异常
    ↓
close()

如果资源没有实现:

java
AutoCloseable

才需要考虑自己在:

java
finally

中完成释放。

3.3 一般不要直接捕获 Throwable ​

异常体系:

text
Throwable
├── Error
└── Exception

如果直接:

java
try {

    // ...

} catch (
        Throwable throwable) {

    // ...
}

意味着连:

text
Exception

和:

text
Error

都会一起捕获。

例如:

text
OutOfMemoryError

StackOverflowError

这类问题通常已经超出了普通业务代码能够恢复的范围。

所以一般不要:

java
catch (Throwable t)

更合理的是根据职责捕获:

java
catch (IOException e)

或者:

java
catch (RuntimeException e)

等具体范围。

极少数基础设施代码可能会因为:

text
线程隔离

容器保护

框架边界

需要捕获 Throwable,但这种代码应该非常谨慎,并且不能简单把:

text
Error

吞掉。

3.4 捕获异常后不要丢失异常信息 ​

非常危险的写法:

java
try {

    // ...

} catch (
        NumberFormatException e) {

}

这里发生了:

text
异常
    ↓
被捕获
    ↓
什么都没做
    ↓
异常彻底消失

通常称为:

text
吞异常

如果确实需要记录:

java
try {

    // ...

} catch (
        NumberFormatException e) {

    log.error(
            "数字转换失败",
            e
    );
}

不要只写:

java
log.error(
        "数字转换失败:"
                + e
);

更不要只写:

java
log.error(
        e.getMessage()
);

因为:

java
log.error(
        "数字转换失败",
        e
);

可以保留完整:

text
异常类型

异常信息

Stack Trace

cause

3.5 避免同一个异常在调用链中重复记录 ​

例如底层:

java
try {

    // ...

} catch (
        NumberFormatException e) {

    log.error(
            "转换失败",
            e
    );

    throw e;
}

然后上层又:

java
catch (
        NumberFormatException e) {

    log.error(
            "业务执行失败",
            e
    );
}

最终同一个异常可能在日志中出现:

text
两次

三次

甚至更多次

造成大量重复日志。

一般可以遵循:

谁最终处理异常,谁负责记录完整异常日志。

如果当前层只是负责:

text
转换异常

可以:

java
try {

    // ...

} catch (
        NumberFormatException e) {

    throw new MyBusinessException(
            "数字格式错误",
            e
    );
}

然后在真正的边界层统一记录。

例如:

text
DAO
    ↓
抛异常


Service
    ↓
转换业务异常


GlobalExceptionHandler
    ↓
统一记录日志

当然这不是绝对规则。

如果当前层掌握了其他层无法获得的重要上下文,也可以记录必要信息,但应该避免:

text
同一个 Stack Trace

在整个调用链反复打印。

3.6 不要在 finally 中 return ​

例如:

java
private int value;

public int test() {

    try {

        return ++value;

    } finally {

        return ++value;
    }
}

执行:

text
try
    ↓
准备返回 1
    ↓
执行 finally
    ↓
finally 又 return 2
    ↓
最终返回 2

也就是说:

text
finally 中的 return

会覆盖:

text
try 中原本的 return

更加危险的是:

java
try {

    throw new RuntimeException(
            "业务异常"
    );

} finally {

    return;
}

这甚至可能把原本的异常:

text
吞掉

所以:

finally 应该负责清理,不应该负责改变方法的返回结果。

3.7 throws 尽量声明具体异常,而不是笼统 Exception ​

不推荐:

java
public void read()
        throws Exception {
}

因为调用者只知道:

text
可能有个 Exception

却不知道具体:

text
文件问题?

数据库问题?

网络问题?

如果明确会抛出:

java
IOException

更推荐:

java
public void read()
        throws IOException {
}

调用者能够更加准确地决定:

text
怎么处理

是否重试

是否转换异常

自定义业务 API 也可以通过一个明确的业务异常进行抽象:

java
public void createOrder()
        throws OrderCreateException {
}

重点在于:

方法声明的异常应该能够表达这个方法真正可能出现的问题。

3.8 catch 尽量捕获具体异常 ​

例如:

java
try {

    readFile();

} catch (
        Exception e) {

    log.error(
            "执行失败",
            e
    );
}

虽然方便,但:

java
Exception

范围很大。

它可能同时捕获:

text
IOException

NullPointerException

IllegalArgumentException

ClassCastException

其他 RuntimeException

这样会让完全不同的问题走进同一套处理逻辑。

如果业务知道可能出现:

text
FileNotFoundException

IOException

可以更加具体:

java
try {

    readFile();

} catch (
        FileNotFoundException e) {

    log.error(
            "文件不存在",
            e
    );

} catch (
        IOException e) {

    log.error(
            "文件读取失败",
            e
    );
}

因为:

text
FileNotFoundException
    ↓
比 IOException 更具体

所以必须放在:

java
IOException

前面。

不过在系统边界层,例如:

text
统一异常处理器

线程入口

消息消费入口

任务调度入口

使用:

java
catch (Exception e)

进行最后兜底是合理的。

所以更准确的原则是:

业务处理中尽量捕获具体异常,边界层可以进行统一兜底。

3.9 自定义异常时必须保留原始 cause ​

不推荐:

java
catch (
        NoSuchMethodException e) {

    throw new MyServiceException(
            "调用方法失败:"
                    + e.getMessage()
    );
}

这里虽然生成了一个新的异常,但是:

text
原来的异常对象

已经丢失。

更推荐:

java
catch (
        NoSuchMethodException e) {

    throw new MyServiceException(
            "调用方法失败",
            e
    );
}

自定义异常:

java
public class MyServiceException
        extends RuntimeException {

    public MyServiceException(
            String message,
            Throwable cause) {

        super(
                message,
                cause
        );
    }
}

这样:

text
MyServiceException
    ↓
cause
    ↓
NoSuchMethodException

整个异常链都能够保留下来。

一般没有必要为了保留异常堆栈而重写:

java
printStackTrace()

Java 自身的:

text
Throwable cause

机制已经可以完整保存异常链。

3.10 finally 中不要让新的异常覆盖原始异常 ​

例如:

java
try {

    someMethod();

} finally {

    cleanUp();
}

假设:

text
someMethod()
    ↓
抛出 Exception A

然后:

text
cleanUp()
    ↓
又抛出 Exception B

传统:

text
try-finally

下可能导致:

text
Exception B
    ↓
覆盖 Exception A

真正首先发生的问题反而丢失。

所以资源关闭优先使用:

java
try-with-resources

因为它会:

text
业务异常
    ↓
Primary Exception


关闭资源异常
    ↓
Suppressed Exception

例如:

text
java.lang.Exception: business

Suppressed:
    java.lang.Exception: close

不会简单覆盖原始异常。

如果必须自己写:

java
finally

也应该认真处理清理过程可能出现的异常,而不是让它随意覆盖原异常。

3.11 生产环境不要使用 printStackTrace() ​

学习和本地调试时:

java
e.printStackTrace();

没有问题。

但生产环境不推荐。

因为:

java
printStackTrace()

直接把异常堆栈输出到:

text
标准错误流
stderr

它缺少正常日志系统具备的:

text
日志级别

文件滚动

结构化输出

时间信息

线程信息

链路信息

日志搜索

日志采集

等能力。

生产代码更推荐:

java
try {

    // ...

} catch (
        Exception e) {

    log.error(
            "订单处理失败",
            e
    );
}

常见日志体系:

text
SLF4J

Logback

Log4j2

日志本身同样要注意:

text
密码

Token

身份证号

银行卡号

密钥

等敏感信息不要直接输出。

3.12 不处理异常但需要清理时,可以使用 try-finally ​

假设:

java
method1();

可能抛出异常。

当前层:

text
不准备处理这个异常

但无论成功还是失败,都必须执行:

java
cleanUp();

可以:

java
try {

    method1();

} finally {

    cleanUp();
}

这里并不需要:

java
catch

异常会继续向上传播。

执行流程:

text
method1()
    ↓
成功 / 异常
    ↓
finally
    ↓
cleanUp()
    ↓
异常继续向上传播

不过如果:

text
cleanUp()

只是为了关闭:

java
AutoCloseable

资源,那么仍然应该优先考虑:

text
try-with-resources

3.13 遵循“早 throw,晚 catch” ​

一个非常经典的异常设计原则:

text
早 throw

晚 catch

3.13.1 早 throw ​

发现参数已经不合法,就尽早抛出。

例如:

java
public static int parseAge(
        String value) {

    if (value == null
            || value.isBlank()) {

        throw new IllegalArgumentException(
                "年龄不能为空"
        );
    }

    return Integer.parseInt(
            value
    );
}

而不是:

text
参数已经有问题
    ↓
继续执行很多业务逻辑
    ↓
更深层才发生异常

越早发现错误:

text
异常位置越接近真正原因

系统状态越容易保持干净

问题越容易排查

3.13.2 晚 catch ​

不要每一层都:

java
try-catch

例如:

text
DAO
    ↓
Service
    ↓
Controller

如果:

text
DAO 根本不知道怎么恢复

没有必要立即捕获。

可以让异常传播到:

text
真正知道应该怎么处理的层

例如:

text
Service
    ↓
转换业务异常


ControllerAdvice
    ↓
统一转换响应

所以:

text
早 throw
    ↓
尽早发现非法状态


晚 catch
    ↓
在真正能够处理的位置捕获

3.14 抛出的异常应该与当前抽象层相关 ​

异常应该尽量表达:

当前方法到底发生了什么问题。

例如:

java
public User loadUser(
        Long userId) {
}

如果底层发生:

text
SQLException

Service 层可能不希望把:

text
数据库实现细节

直接暴露给更上层。

可以转换为:

java
throw new UserQueryException(
        "查询用户失败",
        e
);

这样形成:

text
数据库层
    ↓
SQLException


业务层
    ↓
UserQueryException

既保留:

text
cause

又提高异常语义。

异常抽象应该和当前方法职责一致,而不是随便抛一个:

java
Exception

或者与业务完全无关的异常。

3.15 不要使用异常处理正常业务流程 ​

异常机制应该处理:

text
异常情况

而不是普通分支判断。

例如:

java
try {

    int age =
            Integer.parseInt(
                    value
            );

} catch (
        NumberFormatException e) {

    // 把它当普通 if 使用
}

如果业务本来明确允许:

text
数字

非数字

两种正常输入,并且可以低成本提前验证,就应该考虑普通控制结构:

text
if

switch

条件判断

而不是把异常当成:

text
goto

来使用。

例如业务只允许纯数字:

java
if (
        value != null
                && value.matches("\\d+")
) {

    int number =
            Integer.parseInt(
                    value
            );
}

不过也不能机械理解成:

text
Integer.parseInt()
绝对不能捕获 NumberFormatException

如果你调用的 API 本身就是通过异常表达解析失败,那么合理捕获:

java
NumberFormatException

仍然没有问题。

原则是:

不要故意制造异常来代替正常、可预测的控制流程。

3.16 尽早完成校验,再执行有副作用的操作 ​

例如用户注册需要:

text
验证用户

验证地址

保存用户

保存地址

不推荐:

text
验证用户
    ↓
保存用户
    ↓
验证地址失败
    ↓
再想办法回滚

如果可以提前完成校验,更合理:

text
验证用户
    ↓
验证地址
    ↓
校验全部通过
    ↓
保存用户
    ↓
保存地址

这样可以减少:

text
无意义数据库写入

回滚次数

中间状态

例如:

java
validateUserInput();

validateAddressInput();

saveUser();

saveAddress();

不过:

提前校验不能替代数据库事务和数据库约束。

因为在:

text
校验完成

和:

text
真正写入数据库

之间,仍然可能发生:

text
并发修改

唯一键冲突

网络异常

数据库异常

所以真正涉及多步数据库写操作时仍然需要:

text
事务

保证一致性。

3.17 同一个异常尽量只记录一次完整堆栈 ​

一个异常如果经过:

text
DAO

Service

Controller

全局异常处理器

每一层都:

java
log.error(
        "...",
        e
);

最终日志中会出现大量完全相同的:

text
Stack Trace

真正的问题反而被日志噪音淹没。

例如:

text
DAO
    ↓
记录一次


Service
    ↓
再记录一次


Controller
    ↓
再记录一次


GlobalExceptionHandler
    ↓
再记录一次

一个错误最终打印:

text
4 遍

更好的方式通常是:

text
中间层
    ↓
补充上下文 / 转换异常
    ↓
继续抛


边界层
    ↓
统一记录完整异常

另外,同一个事件的日志信息尽量集中表达。

例如:

java
log.debug(
        "使用缓存区域 A,失败后重试区域 B"
);

比把紧密相关的信息拆成很多零散日志更容易检索。

3.18 异常中要包含足够的上下文 ​

只有:

text
保存失败

通常没有太大价值。

更好的异常信息应该帮助开发人员回答:

text
什么操作失败?

哪个业务对象失败?

关键参数是什么?

失败发生在哪个阶段?

例如:

java
throw new OrderException(
        "订单支付失败,orderId="
                + orderId,
        e
);

日志:

java
log.error(
        "读取配置文件失败,file={}",
        filePath,
        e
);

好的异常信息通常包含:

text
业务动作

关键标识

必要上下文

Throwable cause

但也不要把敏感信息直接写入异常:

text
密码

Token

Secret Key

银行卡号

完整身份证信息

异常信息应该做到:

text
足够排查

但不过度泄露

3.19 不要吞掉 InterruptedException ​

线程代码中:

java
InterruptedException

非常特殊。

不要这样:

java
while (true) {

    try {

        Thread.sleep(
                100000
        );

    } catch (
            InterruptedException e) {

    }

    doSomething();
}

这里:

text
线程已经收到中断请求
    ↓
却被 catch 吞掉
    ↓
线程继续运行

可能导致:

text
线程池无法正常关闭

任务无法取消

应用停止变慢

如果当前方法不能继续向外:

java
throws InterruptedException

通常应该恢复中断状态:

java
catch (
        InterruptedException e) {

    Thread.currentThread()
            .interrupt();

    return;
}

或者:

java
catch (
        InterruptedException e) {

    Thread.currentThread()
            .interrupt();

    break;
}

为什么还要:

java
Thread.currentThread()
        .interrupt();

因为:

text
InterruptedException 被抛出时
    ↓
线程的中断标记通常会被清除

重新调用:

java
interrupt()

可以把中断状态恢复,让上层代码仍然能够感知:

text
这个线程曾经被要求中断

因此可以记:

能继续抛 InterruptedException 就继续抛;不能抛时,通常恢复中断状态并尽快结束当前任务。

3.20 重复异常处理逻辑应该统一封装 ​

如果很多地方都有:

java
try {

    // ...

} catch (
        Exception e) {

    // 完全相同处理
}

或者:

java
try {

    // ...

} finally {

    close();
}

说明公共逻辑可能应该抽取。

例如旧代码中经常出现:

java
public static void closeConnection(
        Connection connection) {

    try {

        if (connection != null) {

            connection.close();
        }

    } catch (
            SQLException e) {

        log.error(
                "数据库连接关闭失败",
                e
        );
    }
}

然后统一调用:

java
finally {

    DBUtil.closeConnection(
            connection
    );
}

现代 Java 中,如果资源实现:

java
AutoCloseable

应该优先:

java
try (
        Connection connection =
                getConnection()
) {

    // 数据库操作
}

实际项目中,还可以根据场景使用:

text
公共工具方法

模板方法

AOP

统一异常处理器

Spring @ControllerAdvice

事务模板

重试模板

来减少重复的:

text
try-catch

代码。

核心原则是:

公共异常处理属于基础设施逻辑时,不要散落在每一段业务代码里重复实现。

3.21 小结 ​

异常处理最重要的不是:

text
try-catch 写得越多越安全

而是:

text
异常在正确的位置产生

异常在正确的位置传播

异常在正确的位置处理

异常信息没有丢失

公共处理逻辑没有重复

可以把前面的原则浓缩为:

text
可以提前判断的错误
    ↓
不要依赖异常控制流程


需要关闭的资源
    ↓
优先 try-with-resources


捕获异常
    ↓
尽量捕获具体类型


无法处理
    ↓
继续向上传播


转换异常
    ↓
保留原始 cause


记录日志
    ↓
保留完整 Stack Trace


同一个异常
    ↓
避免在调用链重复打印


finally
    ↓
不要 return
不要覆盖原异常


InterruptedException
    ↓
不要吞
恢复中断状态或继续抛出


业务输入
    ↓
尽早校验


数据库操作
    ↓
校验不能替代事务


重复异常逻辑
    ↓
统一封装

最终可以记住一句:

早发现、早抛出;能处理再捕获;不能处理就传播;转换异常保留 cause;最终在合适的边界统一记录和处理。

Noctiflorous Garden · MIT License