SPARROW ZOO · 调试踩坑

依赖 jar 找不到源代码,断点为何错位

记录 Maven 依赖调试时「找不到源代码」导致 class 文件与源文件代码行对不齐、 进而引发 debug 断点误判的根因,以及一劳永逸的解决方案。

目录 CONTENTS
  1. 结论先行:源码与字节码行号不一致
  2. 问题背景:依赖一个 jar 到底发生了什么
  3. class 文件里的调试信息
  4. 断点如何映射回源码行
  5. 两种「行号错位」场景
  6. 解决方案
  7. 总结归纳

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 文件里写入 几个属性,其中最关键的三个是:

用 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 下载并启用源码

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 里下载源码,行号自然对得上。