1. 背景

生产环境部分接口突然 500,多个 Controller 报同一个错。

报错信息
1
2
3
java.lang.IllegalArgumentException: Name for argument of type [java.lang.Long] not specified,
and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.

关键信息:

  • 本地和 dev 环境完全正常,仅生产环境复现
  • 不是所有接口,而是 @RequestParam@PathVariable 且没有显式指定 value 的接口 才报错
  • 已经显式指定了 value 的接口(如 @RequestParam("tenantId"))和使用 @RequestBody 的接口完全正常
  • 报错发生在 Spring MVC 的参数解析阶段(AbstractNamedValueMethodArgumentResolver

2. 排查过程

2.1 第一反应:编译参数缺失?

报错信息明确提示 “Ensure that the compiler uses the ‘-parameters’ flag”,第一反应是检查 Maven 编译配置。

查看根 pom.xml

1
2
3
4
5
6
7
8
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<parameters>true</parameters> <!-- ✅ 已配置 -->
</configuration>
</plugin>

配置没问题。子模块的 pom.xml 虽然重新声明了 compiler 插件但漏了 <parameters>true</parameters>,补上后问题依旧。

排除

编译配置不是根因,因为补上后问题没有解决。

2.2 验证 class 文件:参数名到底有没有?

进入生产容器,解压 jar 包,用 javap 检查 Controller 的 class 文件:

1
2
3
4
5
6
7
# 解压 jar
mkdir -p /tmp/check
cp BOOT-INF/lib/app-facade-0.0.1-SNAPSHOT.jar /tmp/check/
cd /tmp/check && jar xf app-facade-0.0.1-SNAPSHOT.jar

# 检查参数名信息
javap -v com/example/controller/SomeController.class | grep -A5 "MethodParameters"

输出结果:

1
2
3
4
MethodParameters:
Name Flags
tenantId
keyword
确认

class 文件中 确实包含 MethodParameters 属性,参数名 tenantIdkeyword 等都在。编译完全没问题。

这就奇怪了——class 文件里有参数名,但运行时 Spring 说拿不到?

2.3 对比环境差异

环境 JVM 是否报错
本地开发 OpenJDK 17 ❌ 正常
dev(Docker 内编译) GraalVM CE 17.0.9 ❌ 正常
生产(预编译 jar) GraalVM CE 17.0.9 ✅ 报错

生产容器的 JVM 版本:

1
2
3
openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment GraalVM CE 17.0.9+9.1
OpenJDK 64-Bit Server VM GraalVM CE 17.0.9+9.1 (mixed mode, sharing)

进一步排查:

  • 没有 spring-boot-devtools(排除 ClassLoader 问题)
  • 没有 AOT 编译或 native-image 处理
  • Docker 基础镜像三个环境一致

2.4 定位根因:GraalVM JVMCI 的反射兼容性问题

标准 OpenJDK 通过 java.lang.reflect.Parameter.getName() 读取 class 文件中的 MethodParameters 属性,这个行为是稳定可靠的。

GraalVM CE 的 JVMCI 编译器 在特定条件下,通过反射获取方法参数名时存在兼容性问题——即使 class 文件中包含了 MethodParameters 属性,运行时 Parameter.getName() 仍然可能返回合成名(如 arg0arg1),导致 Spring MVC 无法匹配参数。

核心结论

class 文件编译正确 ≠ 运行时一定能通过反射拿到参数名。GraalVM 的 JIT 编译器(JVMCI)在这个环节存在与标准 OpenJDK 不一致的行为。

这也解释了为什么:

  • 本地正常 —— 用的是标准 OpenJDK,反射行为符合预期
  • dev 正常 —— 虽然也是 GraalVM,但 dev 是 Docker 内多阶段构建,可能触发了不同的 JIT 编译路径
  • 生产翻车 —— 预编译 jar + GraalVM 运行,恰好触发了反射参数名丢失的问题

3. 问题代码

项目中大量 Controller 方法的 @RequestParam@PathVariable 注解 没有显式指定 value,完全依赖反射获取参数名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 没有显式指定 value,依赖反射获取参数名
@GetMapping("/list-all")
public Response<List<SomeVO>> listAll(
@RequestParam(required = false) Long tenantId,
@RequestParam(required = false) String keyword) {
// ...
}

@GetMapping("/{id}")
public Response<SomeDTO> detail(@PathVariable Long id) {
// ...
}

@GetMapping("/detail")
public Response<SomeVO> detail(@RequestParam Long id) {
// ...
}

Spring MVC 解析这些参数的流程:

  1. 检查注解是否有 value / name 属性 → 没有
  2. 尝试通过 Parameter.getName() 反射获取参数名 → GraalVM 返回 arg0
  3. 无法匹配请求参数 → 抛出 IllegalArgumentException

4. 修复方案

修复方案

给所有 @RequestParam@PathVariable 显式指定 value,彻底不依赖反射获取参数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 显式指定 value,不依赖反射
@GetMapping("/list-all")
public Response<List<SomeVO>> listAll(
@RequestParam(value = "tenantId", required = false) Long tenantId,
@RequestParam(value = "keyword", required = false) String keyword) {
// ...
}

@GetMapping("/{id}")
public Response<SomeDTO> detail(@PathVariable("id") Long id) {
// ...
}

@GetMapping("/detail")
public Response<SomeVO> detail(@RequestParam("id") Long id) {
// ...
}

5. 为什么本地复现不了?

这个 Bug 最恶心的地方在于 本地几乎不可能复现

  1. 开发机用的是标准 OpenJDK,反射获取参数名完全正常
  2. -parameters 编译参数确实配了,class 文件里确实有参数名信息
  3. 单元测试不会走 Spring MVC 的参数解析,MockMvc 测试也可能因为 JVM 不同而表现不一致
  4. 只有在 GraalVM + 特定 JIT 编译路径 下才会触发
教训

“本地能跑”和”生产能跑”之间,可能隔着一个 JVM 实现的差异。

6. 总结

6.1 根因链

1
2
3
4
5
6
7
GraalVM CE 17.0.9 的 JVMCI 编译器
↓ 反射获取方法参数名时存在兼容性问题
↓ Parameter.getName() 返回合成名 arg0/arg1
↓ Spring MVC 无法匹配 @RequestParam/@PathVariable 的参数名
↓ 抛出 IllegalArgumentException
↓ 未显式指定 value 的 @RequestParam/@PathVariable 接口返回 500
↓ 已显式指定 value 的接口和 @RequestBody 接口不受影响

6.2 经验教训

  1. 始终在 @RequestParam@PathVariable 中显式指定 value。这是防御性编码,不依赖任何 JVM 的反射行为,在任何环境下都能正常工作。

  2. 不要假设 -parameters 编译参数在所有 JVM 上都能正确生效。标准 OpenJDK、GraalVM、Eclipse OpenJ9 等不同 JVM 实现对反射的支持程度可能存在差异。

  3. 生产环境的 JVM 选型需要充分测试。GraalVM 虽然性能优秀,但在反射、动态代理等 Java 传统特性上可能存在边缘兼容性问题。

  4. CI/CD 流水线应该与生产环境使用相同的 JVM。如果生产用 GraalVM,那 CI 构建和测试也应该在 GraalVM 上跑,尽早暴露兼容性问题。

一句话总结

永远不要依赖”反射能拿到参数名”这个假设。显式声明,是最可靠的防御。