文章

Java“找不到符号”排查实录:同一个 Maven 版本,本地和制品库竟然不是同一个 JAR

语之溪

·

DevOps平台

·

问题背景

在启动一个 Spring Boot 多模块项目时,Maven 编译阶段出现了下面的错误:

java: 找不到符号
  符号:   方法 getCustomfield_10418()
  位置: 类 com.bocloud.devops.platform.core.jira.issue.Fields

对应的业务代码如下:

if (issue.getFields().getCustomfield_10418() != null) {
    vo.setDefectType(
            issue.getFields().getCustomfield_10418().getValue()
    );
}

直观看,这种错误一般有几种可能:

  1. 方法名拼写错误;
  2. 导入了错误的同名类;
  3. Lombok 没有正常生成 getter;
  4. 依赖版本不正确;
  5. IDE 索引或编译缓存异常;
  6. 私有 Maven 仓库中的依赖被覆盖发布,本地仍持有旧 JAR。

这次最终命中的是第六种,也是最容易让人误判的一种。


一、先明确:这是编译错误,不是运行时错误

虽然问题是在“运行项目”时出现的,但错误实际发生在 Maven 的 compile 阶段:

COMPILATION ERROR

TestReportBaseServiceImpl.java:[573,38] 找不到符号
  符号: 方法 getCustomfield_10418()
  位置: 类 com.bocloud.devops.platform.core.jira.issue.Fields

这意味着 JVM 还没有真正启动,问题与 Spring Bean 初始化、配置中心、数据库连接等都没有关系。

编译器表达的意思非常明确:

当前参与编译的 Fields.class 中不存在 getCustomfield_10418() 方法。

所以排查重点不应该放在运行参数或者 Spring 配置上,而应该放在:

  • 当前代码实际引用的是哪个 Fields;
  • Fields 来自哪个依赖;
  • 当前参与编译的 JAR 中到底有没有该方法。

二、确认业务代码没有导入错误的类

首先检查源码中的 import:

import com.bocloud.devops.platform.core.jira.issue.Issue;

issue.getFields() 返回的是:

com.bocloud.devops.platform.core.jira.issue.Fields

异常信息中提示的也是同一个完整类名:

com.bocloud.devops.platform.core.jira.issue.Fields

因此可以排除“导入了错误同名类”的可能。

接下来检查附近的其他调用:

String text = issue.getFields().getCustomfield_10459();

if (issue.getFields().getCustomfield_10430() != null) {
    vo.setLevel(
            issue.getFields().getCustomfield_10430().getValue()
    );
}

if (issue.getFields().getCustomfield_10418() != null) {
    vo.setDefectType(
            issue.getFields().getCustomfield_10418().getValue()
    );
}

其中:

  • getCustomfield_10459() 可以被编译器识别;
  • getCustomfield_10430() 可以被编译器识别;
  • 只有 getCustomfield_10418() 不存在。

这说明并不是整个 Fields 类失效,也不是 Lombok 完全没有工作,而是当前 JAR 中确实只缺少 customfield_10418。


三、通过 Maven 依赖树找到实际使用的 JAR

项目通过 devops-platform-feign 间接引用了 devops-platform-models。

可以执行:

mvn -pl devops-synergy-core/devops-synergy-service `
    dependency:tree `
    "-Dincludes=com.bocloud.devops:devops-platform-feign,com.bocloud.devops:devops-platform-models"

得到的依赖关系如下:

com.bocloud.devops:devops-synergy-service:jar:3.10.0-patch-guojin
\- com.bocloud.devops:devops-platform-feign:jar:3.10.0-patch-guojin:compile
   \- com.bocloud.devops:devops-platform-models:jar:3.10.0-patch-guojin:compile

也就是说,Fields 最终来自:

com.bocloud.devops:devops-platform-models:3.10.0-patch-guojin

这一步很重要,因为很多时候项目的 pom.xml 中并不会直接声明模型依赖,而是通过 Feign SDK、API SDK 或其他中间依赖传递进来。


四、不要只看源码,直接检查正在参与编译的 class

确定依赖坐标之后,找到本地 Maven 仓库中的 JAR:

<localRepository>\
  com\bocloud\devops\
  devops-platform-models\
  3.10.0-patch-guojin\
  devops-platform-models-3.10.0-patch-guojin.jar

然后使用 JDK 自带的 javap 查看编译后的类:

javap `
  -classpath "devops-platform-models-3.10.0-patch-guojin.jar" `
  -private `
  com.bocloud.devops.platform.core.jira.issue.Fields

本地缓存 JAR 中的结果是:

private java.lang.String customfield_10459;
private com.bocloud.devops.platform.core.jira.issue.LevelInfo customfield_10430;

public java.lang.String getCustomfield_10459();
public com.bocloud.devops.platform.core.jira.issue.LevelInfo getCustomfield_10430();

public void setCustomfield_10459(java.lang.String);
public void setCustomfield_10430(
        com.bocloud.devops.platform.core.jira.issue.LevelInfo
);

没有下面这些内容:

private LevelInfo customfield_10418;

public LevelInfo getCustomfield_10418();

public void setCustomfield_10418(LevelInfo customfield_10418);

到这里已经可以确定:

编译器没有误报,当前本地依赖中的 Fields.class 确实没有 getCustomfield_10418()。

不过,这时仍然有两个可能:

  1. 制品库中的 JAR 本身就没更新;
  2. 制品库已经更新,但本地 Maven 缓存还是旧版本。

因此还需要进一步对比本地 JAR和制品库 JAR。


五、使用独立的临时 Maven 仓库重新下载依赖

如果直接执行:

mvn -U compile

Maven 可能仍然使用已经存在的 release JAR,不能证明制品库中当前是什么内容。

更加可靠的方法是使用一个全新的临时本地仓库:

$tempRepo = Join-Path $env:TEMP (
    "maven-check-" + [guid]::NewGuid().ToString("N")
)

New-Item -ItemType Directory -Path $tempRepo | Out-Null

mvn "-Dmaven.repo.local=$tempRepo" `
    dependency:get `
    "-Dartifact=com.bocloud.devops:devops-platform-models:3.10.0-patch-guojin:jar" `
    "-Dtransitive=false"

这个命令的关键是:

-Dmaven.repo.local=<新的临时目录>

它让 Maven 不使用原来的本地仓库,而是从私有制品库重新下载目标依赖。

这样就可以得到两份 JAR:

  • 原本地 Maven 仓库中的旧 JAR;
  • 从制品库重新下载的新 JAR。

六、通过文件大小和 SHA-256 确认不是同一个制品

使用 PowerShell 计算哈希:

Get-FileHash `
    -Algorithm SHA256 `
    -LiteralPath $localJar, $freshJar

实际对比结果如下:

项目本地缓存 JAR制品库重新下载的 JAR
文件大小717,665 字节733,507 字节
SHA-256B7A43B1E...E7E0A18EF3EF3D...3778DC
getCustomfield_10418()不存在存在

虽然二者 Maven 坐标完全相同:

com.bocloud.devops:
devops-platform-models:
3.10.0-patch-guojin

但文件大小和 SHA-256 都不一样。

这说明制品库中的同一个 release 版本被重新覆盖发布过。

再次使用 javap 检查刚刚下载的新 JAR:

javap `
  -classpath $freshJar `
  -private `
  com.bocloud.devops.platform.core.jira.issue.Fields

新 JAR 中已经包含:

private com.bocloud.devops.platform.core.jira.issue.LevelInfo
        customfield_10418;

public com.bocloud.devops.platform.core.jira.issue.LevelInfo
        getCustomfield_10418();

public void setCustomfield_10418(
        com.bocloud.devops.platform.core.jira.issue.LevelInfo
);

至此,根因完全确认。


七、根本原因

本次问题的根本原因可以概括为:

devops-platform-models:3.10.0-patch-guojin 在私有 Maven 制品库中被覆盖发布,但开发机的本地 Maven 仓库仍然保留着覆盖前的旧 JAR。

具体过程可能是:

  1. 第一次发布 3.10.0-patch-guojin 时,Fields 中只有:

    private String customfield_10459;
    private LevelInfo customfield_10430;
  2. 开发机下载并缓存了这个版本。
  3. 后续开发人员在 Fields 中增加:

    private LevelInfo customfield_10418;
  4. 没有升级版本号,仍然以:

    3.10.0-patch-guojin

    覆盖发布到私有制品库。

  5. 其他机器或者全新环境下载到的是新 JAR,而已经缓存过旧版本的机器继续使用旧 JAR。
  6. 最终出现“相同代码、相同版本号,有些机器可以编译,有些机器不能编译”的现象。

这类问题最麻烦的地方就在于:

pom.xml 中的版本看起来完全正确,依赖树也没有版本冲突,但实际二进制内容不同。

八、解决方法

方案一:删除指定版本的本地缓存

不需要删除整个 Maven 本地仓库,只删除有问题的具体版本目录即可:

$cacheDir = Join-Path $mavenRepo `
    "com\bocloud\devops\devops-platform-models\3.10.0-patch-guojin"

Remove-Item -LiteralPath $cacheDir -Recurse -Force

本次开发环境的实际目录类似:

<localRepository>\
com\bocloud\devops\
devops-platform-models\
3.10.0-patch-guojin

删除之后重新编译:

mvn -U `
    -pl devops-synergy-core/devops-synergy-service `
    -am `
    "-DskipTests" `
    clean compile

Maven 会从制品库重新下载该依赖,新下载的 Fields.class 中已经包含 getCustomfield_10418()。


方案二:使用 Maven purge 清理指定依赖

也可以使用 Maven Dependency Plugin:

mvn dependency:purge-local-repository `
    "-DmanualInclude=com.bocloud.devops:devops-platform-models" `
    "-DreResolve=true"

然后重新编译:

mvn -U clean compile

不过在多模块项目或者依赖关系复杂的项目中,直接删除明确的版本目录通常更直观,也更容易确认删除范围。


方案三:发布一个新的依赖版本

这是长期来看最正确的方案。

例如,将依赖版本从:

<devops-platform.version>
    3.10.0-patch-guojin
</devops-platform.version>

调整为:

<devops-platform.version>
    3.10.0-patch-guojin-20260907
</devops-platform.version>

或者按照团队的版本规则发布:

3.10.0-patch-guojin.1
3.10.0-patch-guojin.2
3.10.1-patch-guojin

只要版本号变化,Maven 就会把它当作一个新的制品,不会继续命中旧版本缓存。


九、为什么 mvn clean 没有用

很多人遇到这类错误时,第一反应是:

mvn clean

但 mvn clean 通常只删除当前项目的构建输出,例如:

target/

它不会删除 Maven 本地仓库中的依赖:

<localRepository>/com/bocloud/devops/...

所以如果问题出在本地依赖 JAR,反复执行下面的命令仍然可能失败:

mvn clean
mvn clean compile
mvn clean package

因为每次编译时,Maven 仍然会读取同一个旧 JAR。


十、为什么仅执行 mvn -U 也不一定有效

-U 的含义通常是强制检查更新,尤其常用于更新 SNAPSHOT:

mvn -U clean compile

但当前依赖版本:

3.10.0-patch-guojin

并不是标准的:

*-SNAPSHOT

而是 release 版本。

对于已经存在于本地仓库中的 release JAR,Maven 通常认为它应该是不可变的。因此,仅执行 -U 不一定会重新下载同坐标的 JAR。

这也是为什么处理被覆盖发布的 release 制品时,最可靠的方法是:

  1. 删除这个版本的本地目录;
  2. 再执行 mvn -U;
  3. 或者使用新的依赖版本号。

十一、为什么清理 IDEA 缓存不是根本解决办法

IDEA 的 Invalidate Caches 主要清理:

  • IDE 索引;
  • 代码分析缓存;
  • 部分项目模型缓存。

它不会可靠地替换 Maven 本地仓库中的旧 JAR。

本次错误中的 Fields.class 本身就缺少目标方法,即使 IDEA 重新建立索引,得到的结论仍然是:

Cannot resolve method getCustomfield_10418

正确的处理顺序应该是:

  1. 清理有问题的 Maven 依赖版本目录;
  2. 重新执行 Maven 编译;
  3. 在 IDEA Maven 工具窗口点击 Reload All Maven Projects;
  4. 仍有显示异常时,再考虑清理 IDEA 索引。

不要一上来就删除整个 .m2 或执行 IDEA 全量缓存清理,这通常耗时很长,而且没有必要。


十二、这类问题如何快速判断

以后再遇到类似错误,可以按照下面的顺序排查。

第一步:确认完整类名

从编译错误中找到:

位置: 类 xxx.xxx.Fields

确认它和源码中的 import 一致。


第二步:确认依赖来源

执行:

mvn dependency:tree

或者只过滤目标依赖:

mvn dependency:tree `
    "-Dincludes=groupId:artifactId"

明确目标类来自哪个 JAR、哪个版本。


第三步:检查 JAR 中是否真的存在目标方法

执行:

javap -classpath "目标.jar" -private 完整类名

例如:

javap `
  -classpath "devops-platform-models-3.10.0-patch-guojin.jar" `
  -private `
  com.bocloud.devops.platform.core.jira.issue.Fields

如果输出中没有目标方法,编译错误就是合理的。


第四步:排除本地缓存

使用新的临时 Maven 仓库重新下载:

$tempRepo = Join-Path $env:TEMP (
    "maven-check-" + [guid]::NewGuid().ToString("N")
)

mvn "-Dmaven.repo.local=$tempRepo" `
    dependency:get `
    "-Dartifact=groupId:artifactId:version:jar" `
    "-Dtransitive=false"

第五步:比较哈希

Get-FileHash -Algorithm SHA256 -LiteralPath $localJar
Get-FileHash -Algorithm SHA256 -LiteralPath $freshJar

判断标准:

  • 哈希相同:本地缓存和制品库一致,应该检查制品本身或版本引用;
  • 哈希不同:同版本制品被覆盖发布,本地缓存已经过期。

第六步:只清理具体版本

不要动不动就删除整个 Maven 仓库。

优先删除:

<localRepository>/<groupId>/<artifactId>/<version>

然后重新构建。


十三、团队层面的改进建议

1. Release 制品必须不可变

同一组 Maven 坐标:

groupId:artifactId:version

应该永远对应同一份二进制内容。

也就是说,下面这个制品发布之后:

com.example:platform-models:3.10.0

就不应该再用不同的代码覆盖它。

如果代码发生变化,应该发布:

3.10.1

或:

3.10.0.1

而不是继续覆盖 3.10.0。


2. 制品库禁止覆盖 release 版本

如果使用 Nexus、Artifactory 或其他私有制品库,可以将 release 仓库的部署策略设置为:

Disable Redeploy

或同等含义的配置。

这样重复发布相同版本时,仓库会直接拒绝,而不是悄悄覆盖旧文件。

这能够从源头上避免:

  • 开发机依赖内容不一致;
  • CI 和本地构建结果不一致;
  • 回滚时拿不到原来的制品;
  • 同一个版本号无法准确追溯;
  • 构建结果不可复现。

3. 开发版本使用 SNAPSHOT

如果依赖仍处于频繁变更阶段,可以使用:

3.10.0-patch-guojin-SNAPSHOT

SNAPSHOT 本来就允许更新,Maven 也会按照更新策略检查新版本。

开发完成并稳定后,再发布不可变的 release:

3.10.0-patch-guojin

4. CI 中记录依赖校验信息

为了提高可追溯性,可以在 CI 中记录:

  • 完整依赖树;
  • 关键内部依赖版本;
  • 构建产物 SHA-256;
  • 内部依赖 JAR 的 SHA-256;
  • Git commit ID;
  • 制品发布时间。

这样即使版本号相同,也能够快速判断实际使用的是哪一份二进制文件。


5. 不要用“清空整个 Maven 仓库”作为常规方案

删除整个本地 Maven 仓库虽然有时能解决问题,但代价很大:

  • 需要重新下载全部依赖;
  • 内网仓库速度慢时会严重影响开发;
  • 可能暴露出已经下线的历史制品;
  • 无法帮助团队定位真正的问题。

更推荐精准清理:

groupId/artifactId/version

既能解决问题,又能保留排查证据。


十四、一个容易被忽略的运行时问题

本次编译错误解决后,还要确认服务端实际返回 customfield_10418。

业务代码当前请求字段时使用了:

jiraSearchRequest.setFields(
        Collections.singletonList("*navigable")
);

如果 JIRA 中的 customfield_10418 不属于 navigable 字段,即使模型已经包含:

getCustomfield_10418()

运行时也可能得到 null。

更稳妥的方式是明确请求该字段:

jiraSearchRequest.setFields(Arrays.asList(
        "*navigable",
        "customfield_10418"
));

因此,需要区分两个层面:

  1. 编译期

    模型类中必须存在:

    getCustomfield_10418()
  2. 运行期

    JIRA 或平台接口返回的 JSON 中必须包含:

    {
      "customfield_10418": {
        "value": "功能问题"
      }
    }

模型存在只能解决编译问题,并不能保证接口一定返回数据。


十五、最终结论

本次问题并不是业务代码写错,也不是 IDEA 缓存异常,而是:

私有 Maven 制品库覆盖发布了同一个 release 版本,而本地 Maven 仓库仍缓存着覆盖前的旧 JAR。

关键证据包括:

  1. Maven 依赖树确认实际使用的是:

    devops-platform-models:3.10.0-patch-guojin
  2. 本地 JAR 的 Fields.class 中不存在:

    getCustomfield_10418()
  3. 从制品库使用全新 Maven 本地仓库下载的同版本 JAR 中存在该方法。
  4. 两份同版本 JAR 的文件大小和 SHA-256 均不同。
  5. 删除指定版本的本地 Maven 缓存并重新下载后,问题恢复。

这类问题给出的最大启示是:

Maven 版本号不仅是一个标签,也应该是制品内容的唯一身份。同一个 release 版本一旦发布,就不应该再被覆盖。

当遇到“别人能编译、我不能编译”“版本号明明一样,类的方法却不一样”时,与其反复清理 IDE 缓存,不如直接检查正在使用的 JAR,并用 SHA-256 对比本地制品和远程制品。二进制文件不会说谎。


可用于内部复盘的简版总结

问题现象

编译 devops-synergy-service 时提示:

Fields 中找不到 getCustomfield_10418()

影响范围

本地缓存过旧版 devops-platform-models:3.10.0-patch-guojin 的开发机或构建节点无法编译;全新环境可能不受影响。

根本原因

私有制品库覆盖发布了相同版本号的 devops-platform-models:

  • 旧 JAR 不包含 customfield_10418;
  • 新 JAR 包含 customfield_10418;
  • 两份 JAR 的 Maven 坐标相同,但 SHA-256 不同。

临时解决方案

删除本地仓库中的具体版本目录,然后重新下载:

com/bocloud/devops/devops-platform-models/3.10.0-patch-guojin

重新执行:

mvn -U clean compile

长期改进

  • 禁止覆盖发布 release 制品;
  • 每次变更发布新的版本号;
  • 开发过程使用 SNAPSHOT;
  • CI 保存依赖树及关键制品 SHA-256;
  • 出现同类问题时优先使用 dependency:tree、javap 和哈希比对定位。
现在已有 72 次阅读,0 条评论,0 人点赞
作者:语之溪
作者
Java“找不到符号”排查实录:同一个 Maven 版本,本地和制品库竟然不是同一个 JAR
当前文章累计共 13481 字,阅读大概需要 8 分钟。
我如何梳理一个微服务系统
2026年3月21日 - 0评论
HAOVP 的技术选型和服务拆分
2025年12月20日 - 0评论
Typecho 发布文章后前台不显示:Docker PHP 容器时区问题排查与解决
2026年9月7日 - 0评论
评论:共0条
发表
功能 探索 消息

足迹

你还不曾留下足迹..

音乐单

随机文章

夜晚

💬
你还不曾留言过
博主 不再显示
博主
未知作品 歌曲封面
立即安装