01结论先行
调试时断点「落到错行」或「看起来没生效」,本质上是编译产物里的行号表 (LineNumberTable)与你眼前看到的源码不一致。
Maven 依赖默认只提供编译好的 .class 字节码,不带 .java 源码。
当 IDE 找不到配套的 -sources.jar(源码包)时,只能反编译字节码来充当源码,
反编译结果的代码行号与原始源码并不对应,于是断点落在「看着正确、实际不对」的行上。
《The Java Virtual Machine Specification》第 4.7 章定义,class 文件可携带
SourceFile、LineNumberTable、LocalVariableTable
等调试属性;调试器正是靠 LineNumberTable 把「字节码偏移量」翻译回「源码行号」。
而 Maven 官方 maven-source-plugin 用于把源码打进 -sources.jar,
供 IDE 下载和联调,二者缺一不可。
一句话结论:要让断点精准,就必须保证「源码 = 编译这份字节码时的那份源码」, 且源码可被 IDE 找到。缺源码或源码与字节码不同步,都会造成行号错位。
02问题背景:依赖一个 jar 到底发生了什么
先从一个小白的视角还原整个过程。当你在 pom.xml 里写下一段依赖:
<dependency>
<groupId>com.sparrow</groupId>
<artifactId>sparrow-core</artifactId>
<version>1.0.0</version>
</dependency>
Maven 会去仓库把这个 sparrow-core-1.0.0.jar 拉下来。请注意,这个 jar 里装的是:
| 内容 | 是什么 | 能否用于调试 |
|---|---|---|
*.class | 编译后的字节码 + 元数据(含行号表) | 能运行,但不能直接读 |
META-INF/ | 清单、资源、签名等 | 与调试无关 |
*.java | ❌ 默认不包含 | —— |
也就是说,普通 jar 里没有 .java 源码。而调试器想要在断点处高亮出一行代码,
就必须拿到一份「源码」。这份源码从哪来?答案是一个额外的产物——-sources.jar。
Maven 中央仓库或公司私服上的库,通常会同时发布三个产物:
主 jar(字节码)、-sources.jar(源码)、-javadoc.jar(文档)。
若发布时漏掉了 -sources.jar,下游使用者就「找不到源代码」。
03class 文件里的调试信息
为什么反编译会导致行号错位?先要理解字节码里「行号」是怎么存的。
用 javac 编译 Java 源码时,只要开启了调试信息(默认开启,Maven 的
maven-compiler-plugin 默认 debug=true),编译器就会往 class 文件里写入
几个属性,其中最关键的三个是:
SourceFile:记录编译这份字节码的源文件名,如UserService.java。LineNumberTable:记录「字节码指令偏移量 → 源码行号」的映射表。LocalVariableTable:记录局部变量名与槽位、作用域的映射(方便看变量名)。
用 javap 反汇编一个 class,就能看到这张行号表:
javap -c -l com.sparrow.service.UserService.class
public com.sparrow.service.UserService();
Code:
// 字节码偏移 0 行号 12
0: aload_0
...
// ... 中间省略 ...
LineNumberTable:
line 12: 0
line 13: 4
line 14: 11
这张 LineNumberTable 就是调试器「对行号」的唯一依据。它把每个字节码位置对应到
编译当时的源码行号。
行号表是「编译时刻」的产物。如果编译之后源码又改了、却没有重新编译, 那么字节码里的行号表就会和最新源码脱节——这正是错位的温床。
04断点如何映射回源码行
调试器(以 IDEA/Eclipse 为例)在断点命中时,会走这样一条链路:
| 步骤 | 动作 | 依赖 |
|---|---|---|
| ① | JVM 通过 JPDA 上报「执行到某字节码偏移量」 | class 字节码 |
| ② | 调试器查 LineNumberTable,把偏移量换算成源码行号 | class 里的行号表 |
| ③ | 调试器按 SourceFile + 行号,找到并打开源码文件 | -sources.jar |
| ④ | 在源码对应行高亮断点 | 源码与行号表一致 |
可以看到,第 ③ 步需要「真正的源码文件」,第 ④ 步需要「源码的行号与行号表一致」。 这两步任何一环出问题,断点就会「错位」或「飘」。
05两种「行号错位」场景
场景一:找不到源码 → 反编译兜底
当依赖没有 -sources.jar 时,IDEA 会提示「Download Sources」;如果仓库里也没有,它就只能反编译 .class 来给你看代码:
// IDEA 反编译后的代码(Fernflower)—— 只是近似还原,不是原始源码
public class UserService {
public User getUser(Long id) {
User user = userMapper.selectById(id); // 反编译行号可能在此
if (user == null) {
throw new RuntimeException(...); // 但字节码行号表指向另一行
}
return user;
}
}
断点打在反编译代码的某一行,但程序执行到该行时却不命中,或命中在
相邻的、看似无关的行上——因为反编译产物的行号与字节码 LineNumberTable 对不上。
场景二:sources.jar 过期 / 不同步
即使有 -sources.jar,如果它和主 jar 不是同一次构建产出的(例如改了代码后只重新打包了主 jar、
忘了重新生成源码包),那么源码包里的行号是「旧版本」的,字节码里的行号表却是「新版本」的,
两者照样错位。
| 场景 | 字节码行号表 | 你看到的源码 | 结果 |
|---|---|---|---|
| 无 sources.jar | 原始行号 | 反编译产物(行号漂移) | 断点错位 |
| sources.jar 过期 | 新版行号 | 旧版源码 | 断点错位 |
| sources.jar 同步 | 一致 | 同一次构建源码 | ✅ 精准命中 |
06解决方案
从「发布方」和「使用方」两侧同时下手,才能彻底解决。
① 发布方:打包时带上 sources.jar
在库的 pom.xml 里挂上 maven-source-plugin,绑定到 package 阶段,让每次构建都自动产出源码包:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
这样 mvn package / mvn install / mvn deploy 都会在同一次构建里生成 xxx-sources.jar,保证源码与字节码同步。
② 发布方:确认编译开启了调试信息
没有行号表,一切都无从谈起。确保 maven-compiler-plugin 的 debug 没被关掉:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<debug>true</debug>
<debuglevel>lines,vars,source</debuglevel>
</configuration>
</plugin>
③ 关于 maven-gpg-plugin:它不背「行号」的锅
本项目的发布配置里有这样一段签名插件:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
maven-gpg-plugin 只负责给发布到中央仓库的产物做 GPG 签名(完整性、防篡改),
它不参与源码生成、也不影响行号表。真正解决「找不到源码」的是前面的
maven-source-plugin。二者职责不同,别混淆。
④ 使用方:让 IDE 下载并启用源码
- IDEA 里右键依赖 →
Download Sources;或全局开启Settings → Build Tools → Maven → Importing → 勾选 Sources,让 IDEA 自动下载源码。 - 确认没在看「反编译」代码:IDEA 编辑器标签/右上角会显示「Decompiled .class」字样,出现它说明在用反编译产物。
- 本地联调时,优先用
mvn install而非直接引用target/classes,确保 IDE 拿到的是带源码的安装产物。
07总结归纳
| 问题 | 根因 | 解决 |
|---|---|---|
| 依赖 jar 找不到源代码 | 发布时没带 -sources.jar | 发布方挂 maven-source-plugin |
| 断点落到错行 / 不命中 | 源码与字节码 LineNumberTable 不一致 | 保证源码与字节码同一次构建产出 |
| IDE 展示的是反编译代码 | 仓库无源码包,IDE 反编译兜底 | IDE 开启「Download Sources」 |
| 类完全没有行号表 | 编译时关闭了 debug 信息 | maven-compiler-plugin 保持 debug=true |
断点精准 = 「带调试信息的字节码」+「与之同源的 sources.jar」。
发布方用 maven-source-plugin 补源码包,使用方在 IDE 里下载源码,行号自然对得上。