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

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法