Java 14中的 JEP 358: Helpful NullPointerExceptions是如何实现的

Java 14在2020年3月17号正式发布,不久AdoptOpenJDK也跟进发布了新版。

JEP 358: Helpful NullPointerExceptions

其中对使用者比较有用的功能是 JEP 358: Helpful NullPointerExceptions,即在NullPointerException的message中指明为什么产生了这个NPE。

示例代码如下(需要Java14,并配置参数-XX:+ShowCodeDetailsInExceptionMessages):

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class Main {
static class Class1 {
Object object;
}
static class Class2 {
Class1 class1;
public Class1 getClass1() {
return class1;
}
}
static Class2 class2;
private static Object get(int i) {
return null;
}
// 打印NPE的message
private static void printNPE(Runnable runnable) {
try {
runnable.run();
} catch (NullPointerException e) {
System.err.println(e.getMessage());
}
}
public static void main(String[] args) {
Class2 class21 = null;
printNPE(() -> {
// 1. getstatic为null
// Cannot invoke "Main$Class2.getClass1()" because "Main.class2" is null
System.out.println(class2.getClass1().object.toString());
});
Class2 class22 = new Class2();
printNPE(() -> {
// 2. 调用函数结果为null
// Cannot read field "object" because the return value of "Main$Class2.getClass1()" is null
System.out.println(class22.getClass1().object.toString());
});
class22.class1 = new Class1();
printNPE(() -> {
// 3. 函数调用后,getfield为null
// Cannot invoke "Object.toString()" because "Main$Class2.getClass1().object" is null
System.out.println(class22.getClass1().object.toString());
});
printNPE(() -> {
// 4. 链式getfield
// Cannot invoke "Object.toString()" because "class22.class1.object" is null
System.out.println(class22.class1.object.toString());
});
Integer[] ary1 = null;
printNPE(() -> {
// 5. 对null执行aaload
// Cannot load from object array because "ary1" is null
System.out.println(ary1[0].intValue());
});
Integer[] ary2 = new Integer[1];
printNPE(() -> {
// 6. iconst_0+aaload后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[0]" is null
System.out.println(ary2[0].intValue());
});
printNPE(() -> {
int i = 0;
// 7. iconst_0+iload_1后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[i]" is null
System.out.println(ary2[i].intValue());
});
printNPE(() -> {
int i = 0;
// 8. iconst_0+iadd后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[...]" is null
System.out.println(ary2[i + 0].intValue());
});
printNPE(() -> {
// 9. invokevirtual结果为null
// Cannot invoke "Object.toString()" because the return value of "Main.get(int)" is null
System.out.println(Main.get(1).toString());
});
}
}

令我比较意外的是情况4和情况7,Java能够追溯链式回调,而且连变量名都带上了!

JEP 358实现细节

那我们就看下这么神奇的特性是如何实现的吧。

首先,这个功能需要添加参数-XX:+ShowCodeDetailsInExceptionMessages才能开启,那我们只要在JDK的代码中搜索这个字符串就行。于是找到给NPE填充Message的代码:

可以看到,比较重要函数是 get_NPE_message_at,我们继续跟进:

这儿的逻辑很简单,首先打印失败的操作(print_NPE_failed_action)然后再打印失败的原因(print_NPE_cause)。

先看 print_NPE_failed_action ,主体就是一个大的switch:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
void ExceptionMessageBuilder::print_NPE_failed_action(outputStream *os, int bci) {

// Get the bytecode.
address code_base = _method->constMethod()->code_base();
Bytecodes::Code code = Bytecodes::java_code_at(_method, code_base + bci);
int pos = bci + 1;
if (code == Bytecodes::_wide) {
code = Bytecodes::java_code_at(_method, code_base + bci + 1);
pos += 1;
}

switch (code) {
case Bytecodes::_iaload:
os->print("Cannot load from int array"); break;
case Bytecodes::_faload:
os->print("Cannot load from float array"); break;
case Bytecodes::_aaload:
os->print("Cannot load from object array"); break;
case Bytecodes::_baload:
os->print("Cannot load from byte/boolean array"); break;
case Bytecodes::_caload:
os->print("Cannot load from char array"); break;
case Bytecodes::_saload:
os->print("Cannot load from short array"); break;
case Bytecodes::_laload:
os->print("Cannot load from long array"); break;
case Bytecodes::_daload:
os->print("Cannot load from double array"); break;

case Bytecodes::_iastore:
os->print("Cannot store to int array"); break;
case Bytecodes::_fastore:
os->print("Cannot store to float array"); break;
case Bytecodes::_aastore:
os->print("Cannot store to object array"); break;
case Bytecodes::_bastore:
os->print("Cannot store to byte/boolean array"); break;
case Bytecodes::_castore:
os->print("Cannot store to char array"); break;
case Bytecodes::_sastore:
os->print("Cannot store to short array"); break;
case Bytecodes::_lastore:
os->print("Cannot store to long array"); break;
case Bytecodes::_dastore:
os->print("Cannot store to double array"); break;

case Bytecodes::_arraylength:
os->print("Cannot read the array length"); break;
case Bytecodes::_athrow:
os->print("Cannot throw exception"); break;
case Bytecodes::_monitorenter:
os->print("Cannot enter synchronized block"); break;
case Bytecodes::_monitorexit:
os->print("Cannot exit synchronized block"); break;
case Bytecodes::_getfield: {
int cp_index = Bytes::get_native_u2(code_base + pos) DEBUG_ONLY(+ ConstantPool::CPCACHE_INDEX_TAG);
ConstantPool* cp = _method->constants();
int name_and_type_index = cp->name_and_type_ref_index_at(cp_index);
int name_index = cp->name_ref_index_at(name_and_type_index);
Symbol* name = cp->symbol_at(name_index);
os->print("Cannot read field "%s"", name->as_C_string());
} break;
case Bytecodes::_putfield: {
int cp_index = Bytes::get_native_u2(code_base + pos) DEBUG_ONLY(+ ConstantPool::CPCACHE_INDEX_TAG);
os->print("Cannot assign field "%s"", get_field_name(_method, cp_index));
} break;
case Bytecodes::_invokevirtual:
case Bytecodes::_invokespecial:
case Bytecodes::_invokeinterface: {
int cp_index = Bytes::get_native_u2(code_base+ pos) DEBUG_ONLY(+ ConstantPool::CPCACHE_INDEX_TAG);
os->print("Cannot invoke "");
print_method_name(os, _method, cp_index);
os->print(""");
} break;

default:
assert(0, "We should have checked this bytecode in get_NPE_null_slot().");
break;
}
}

可以看到,这一步就是针对不同的操作码,输入不同的信息。比如例子中,情况2走的就是_getfield,情况7走的就是_invokevirtual分支,当然还有一些打印方法名之类的操作,这儿就不多说了。

接下来看第二部分, print_NPE_cause ,失败的原因是怎么拼出来的:

1
2
3
4
5
6
7
bool ExceptionMessageBuilder::print_NPE_cause(outputStream* os, int bci, int slot) {
if (print_NPE_cause0(os, bci, slot, _max_cause_detail, false, " because "")) {
os->print("" is null");
return true;
}
return false;
}

print_NPE_cause调用print_NPE_cause0输出原因,并在前面添加because ",在其后添加" is null

对于这个功能来说, print_NPE_cause0 的实现比较重要。

print_NPE_cause0 方法定义如下:

os接收字符串;bci表示操作数栈的的偏移;slot表示操作目标在第几个slot上;max_detail表示目前还能再追溯几级,inner_expr表示是否在追溯表达式,如果是的话,省略默写字符串;prefix表示前缀。

print_NPE_cause0 的内部是一个大的switch,根据不同的字节码做不同分析行为。我们分类来看下:

  1. 对于 _aload_iload_aload<n>_iload<n>、const相关指令来说,只需要输出立即数即可:

  1. 对于 _iaload_aaload来说,我们需要打印数组名和下标

首先递归调用,打印数组来源(数组名或者方法名),然后再递归调用获得数组的index。

3.对于getstatic,直接输出类名和字段名即可

4.对于getfield,通过递归调用来逐级输出getfield的”链条”,比如例子中的情况4,就能输出class22.class1.object is null

5.对于invoke系列指令,只需要输出方法名就行。

还有一个比较重要的问题是,如何获取局部变量的名字?刚刚有看到调用 print_local_var 的地方,就是它了:

可以看到如果有LocalVariableTable的话,就遍历它,找到变量名字。如果没有的话,走下面的逻辑:

如果没有的话,对于参数,只能输出<paramenter%d>;对于局部变量,输出 <local%d>

比如,对于如下情况:

1
2
3
4
5
6
Integer[] ary2 = new Integer[1];
printNPE(() -> {
int i = 0;
// 7. iconst_0+iload_1后为null
System.out.println(ary2[i].intValue());
});

NPE的输出就变为了Cannot invoke "java.lang.Integer.intValue()" because "<parameter1>[<local1>]" is null

在这儿需要解释下,local1就是i,paramenter1是ary2,因为在Java中,Lambda的变量捕获是使用隐式传参的方式实现的。

总结

总而言之,这个特性对于使用者来说是非常好的;但这儿的“回溯”很多逻辑都是Java代码相关的,而JVM作为一个通用的平台,上面也许会有很多其他的语言来跑,它们不仅不能享受到此特性带来的好处,反而可能会有更加混乱的NPE message。

所以希望这个特性能够标准化,并写入到JVMS中,这样其他基于JVM的语言才能更好的利用这个特性。


javac在编译的时候,添加上-parameters就能在class文件中存储参数的名字,加上-g:vars参数就能带上本地变量表(LocalVariableTable),更加方便调试。

Java 14中的 JEP 358: Helpful NullPointerExceptions是如何实现的

https://robberphex.com/how-jep-358-helpful-nullpointerexceptions-works/

作者

Robert Lu

发布于

2020-03-21

许可协议

评论