Java 14 起,
NullPointerException會用前向數據流分析,在報錯裡直接打印為 null 的變數名。前提是生產打包必須保留LocalVariableTable調試信息,否則變數名會退化成<local1>。
Issue(現象)
鏈式調用(如 user.getAddress().getCity())是 NPE 的高發區。Java 14 之前,例外堆疊只給行號,看不出到底是哪一步返回了 null。Java 14 引入 Helpful NullPointerExceptions,日誌裡直接點名具體變數。
如果在生產環境看到 <local1> 這類佔位符,說明特性已開啟,但編譯參數配錯了。
Environment(環境)
- JDK 14+(預設開啟)
- 構建工具:Maven / Gradle,與復現結果直接相關
StackTrace(堆疊日誌)
同一條鏈式調用,三種環境下的真實報錯對比:
// 传统报错(JDK 8 ~ 13):只能靠猜
Exception in thread "main" java.lang.NullPointerException
at com.stacktraces.demo.NpeTest.resolveLength(NpeTest.java:27)
at com.stacktraces.demo.NpeTest.cityNameLength(NpeTest.java:22)
at com.stacktraces.demo.NpeTest.formatCityInfo(NpeTest.java:18)
at com.stacktraces.demo.NpeTest.main(NpeTest.java:14)
// 现代报错(JDK 14+):精准指认
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "com.stacktraces.demo.Address.getCity()" is null
at com.stacktraces.demo.NpeTest.resolveLength(NpeTest.java:27)
at com.stacktraces.demo.NpeTest.cityNameLength(NpeTest.java:22)
at com.stacktraces.demo.NpeTest.formatCityInfo(NpeTest.java:18)
at com.stacktraces.demo.NpeTest.main(NpeTest.java:14)
// 生产退化(丢失调试信息):变量名变占位符
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "<local1>.getCity()" is null
at com.stacktraces.demo.NpeTest.resolveLength(NpeTest.java:27)
at com.stacktraces.demo.NpeTest.cityNameLength(NpeTest.java:22)
at com.stacktraces.demo.NpeTest.formatCityInfo(NpeTest.java:18)
at com.stacktraces.demo.NpeTest.main(NpeTest.java:14)
Reproduce(復現程式碼)
用 JDK 14+ 編譯運行即可復現:
package com.stacktraces.demo;
public class NpeTest {
static class Address {
String getCity() { return null; }
}
static class User {
Address getAddress() { return new Address(); }
}
public static void main(String[] args) {
User user = new User();
System.out.println(formatCityInfo(user));
}
static String formatCityInfo(User user) {
return "city length = " + cityNameLength(user);
}
static int cityNameLength(User user) {
return resolveLength(user);
}
static int resolveLength(User user) {
// 触发点:getCity() 返回 null,.length() 抛 NPE
return user.getAddress().getCity().length();
}
}
Root Cause(根因分析)
日誌能給出變數名,靠的不是運行時埋點。NPE 拋出時,JVM 的 ExceptionMessageBuilder 做一次輕量級前向數據流分析,模擬每條字節碼對操作數棧的影響,回溯到那一條把 null 壓棧的指令(如 getfield / aload)。
定位到字節碼後,JVM 還需要 class 文件裡的 LocalVariableTable,把指令索引還原成源碼變數名。
這正是退化的根因。很多 CI/CD 打包為了縮小體積或防反編譯,設定了 javac -g:none。調試信息被剔除後,LocalVariableTable 不存在,JVM 查不到變數名,只能打印 <local1> 或 someObject。
例外消息在調用 e.getMessage() 時才即時計算,所以常規執行路徑沒有任何性能損耗,不必擔心這個特性拖慢程式。
The Fix(解決方案)
Java 14 預設開啟,一般情況下不需要配。特殊場景想強制控制,用這個參數:
-XX:+ShowCodeDetailsInExceptionMessages # 开启(默认)
-XX:-ShowCodeDetailsInExceptionMessages # 关闭
要修的是生產環境 <local1> 佔位符。檢查 pom.xml 或 build.gradle,確認編譯器沒有屏蔽 debug 信息。
Maven 下,maven-compiler-plugin 要保持預設開啟,正確設定是:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<!-- 必须为 true(默认即 true,别配成 false) -->
<debug>true</debug>
<!-- 确保包含 vars(LocalVariableTable) -->
<debuglevel>lines,vars,source</debuglevel>
</configuration>
</plugin>
避坑提醒:別隻打印 e.getMessage()。現在 message 有內容了,但行號仍在 StackTrace 裡。保持 log.error("发生系统异常", e); 的習慣。
