概述

  1. Java 的异常处理是对代码性能有着重要影响的因素
  2. Java 的异常处理,有着天生优势,特别是在错误排查方面的作用,很难找到合适的替代方案

用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package me.zhongmingmao;

import java.security.NoSuchAlgorithmException;

public class UseCase {
public static void main(String[] args) {
String[] algorithms = {"SHA-128", "SHA-192"};

String availableAlgorithm = null;
for (String algorithm : algorithms) {
Digest md;
try {
md = Digest.of(algorithm);
} catch (NoSuchAlgorithmException ex) {
// ignore, continue to use the next algorithm.
continue;
}

try {
md.digest("Hello, world!".getBytes());
} catch (Exception ex) {
System.getLogger("me.zhongmingmao")
.log(System.Logger.Level.WARNING, algorithm + " does not work", ex);
continue;
}

availableAlgorithm = algorithm;
}

if (availableAlgorithm != null) {
System.out.println(availableAlgorithm + " is available");
} else {
throw new RuntimeException("No available hash algorithm");
}
}
}

可恢复异常

  1. NoSuchAlgorithmException
  2. 尝试捕获识别这个异常,然后再从这个异常里恢复过来,继续执行代码
  3. 可恢复的异常处理 - 可以从异常里恢复过来,继续执行的异常处理
1
2
3
} catch (NoSuchAlgorithmException nsae) {
// ignore, continue to use the next algorithm.
}
  1. 只要 catch 语句能够捕获识别这个异常,那么这个异常生命周期结束
  2. catch 只需要知道异常的名字,而不需要知道异常的调用堆栈
    • 极大地削弱Java 异常错误排查方面的作用
  3. 可恢复异常不使用异常的调用堆栈,是否可以不生成调用堆栈
    • 基于 Java 异常的性能基准测试结果,生成异常的调用堆栈是异常处理影响性能最主要因素
    • 如果不需要生成调用堆栈,Java 异常的处理性能会有成百上千倍的提升

不可恢复异常

  1. RuntimeException
  2. 上述代码并没有捕获识别,该异常会直接导致程序的退出,并且把异常的信息和堆栈打印出来
  3. 不可恢复的异常处理 - 导致了程序的中断,程序并不能从异常抛出的地方恢复
  4. 调用堆栈对于不可恢复异常来说至关重要
    • 可以从异常调用堆栈的打印信息中,快速定位到出问题的代码,降低了运维的成本
  5. 由于不可恢复异常中断了程序的运行,因此它的开销是一次性
  6. 现实中,基本不会允许程序由于异常而中断退出 - 服务端 + 客户端
    • 高质量的产品里,很难允许不可恢复异常的存在

记录的调试信息

  1. Exception
  2. 尝试捕获识别这个异常,然后从异常里恢复过来继续执行代码 - 可恢复异常
    • 同时,还在日志里记录了该异常,异常的调试信息,即异常信息调用堆栈,也会被详细记录到日志中
  3. 典型的使用场景 - 程序可以恢复,但异常信息可以查询
1
2
3
4
5
} catch (Exception ex) {
System.getLogger("me.zhongmingmao")
.log(System.Logger.Level.WARNING, algorithm + " does not work", ex);
continue;
}
  1. 异常捕获的场景下 - 方法调用
    • 该异常的记录方式,包括是否记录 me.zhongmingmao
    • 该异常的记录地点 - System.getLogger()
    • 该异常的严重程度 - Logger.Level
    • 该异常的影响范围 - [algorithm] does not work
  2. 异常生成的场景下 - 方法实现
    • 异常生成时携带的调试信息,包括异常信息调用堆栈
  3. 记录在案的调试信息 - 调用代码 + 实现代码

改进

共用错误码本身,并没有携带调试信息,为了能够快速定位问题,需为共用错误码补充调试信息

方法实现

使用异常的形式补充了调用信息,包括问题描述调用堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Returned<Digest> of(String algorithm) {
return switch (algorithm) {
case "SHA-256" -> new Returned.ReturnValue<>(new SHA256());
case "SHA-512" -> new Returned.ReturnValue<>(new SHA512());
case null -> {
System.getLogger("me.zhongmingmao")
.log(
System.Logger.Level.WARNING,
"No algorithm is specified",
new Throwable("the calling stack"));
yield new Returned.ErrorCode<>(-1);
}
default -> {
System.getLogger("me.zhongmingmao")
.log(
System.Logger.Level.INFO,
"Unknown algorithm is specified %s".formatted(algorithm),
new Throwable("the calling stack"));
yield new Returned.ErrorCode<>(-1024);
}
};
}

方法调用

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Returned<Digest> returned = Digest.of("SHA-128");
switch (returned) {
case Returned.ReturnValue value -> {}
case Returned.ErrorCode code -> {
System.getLogger("me.zhongmingmao")
.log(
System.Logger.Level.INFO,
"Failed to get instance of SHA-128, code: %d".formatted(code.errorCode()));
}
}
}

运行效果

类似于使用异常处理,可以快速定位问题的调试信息

image-20250805174155133

对比

  1. 使用调试信息带来的性能损失,并不比使用异常的性能损失小多少
  2. 但日志记录既可以开启,也可以关闭
    • 如果关闭了日志,就不用再生成调试信息了,对应的性能影响也就消失了
    • 在需要定位问题的时候,再启动日志
    • 这样,可以把性能影响控制在一个极小的范围
问题 共用错误码的解答
可恢复异常能不能不生成调用堆栈 可以,不开启日志,则不生成调用堆栈
不可恢复异常还有存在必要么? 没有,错误码方案的所有错误,都可以恢复
有没有快速定位问题的替代方案 有,开启日志,就能提供调试信息了
  1. 日志并不是唯一可以记录调试信息的方式,可以使用更便捷的 JFR
  2. 错误码的调试方式,更符合调试的目的,只有需要调试的时候,才会生成调试信息