StackTraces
Java已驗證

Java 14 NPE 自帶變數名,生產環境退化成 <local1>

@debug_master32 天前更新閱讀 6 min00

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); 的習慣。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法