bigywater.tech
Java · 328

Java API 兼容性检查

升级依赖之后程序为什么会抛 NoSuchMethodError?二进制兼容和源码兼容是一回事吗?工具说「兼容」就一定能跑吗?这篇做了 15 种 API 变化,用工具和真实客户端互相对照。

10 / 15
对 2.0 的库运行针对 1.0 编译的客户端时失败的个数
3 处
japicmp 报告与实测不一致的变化:接口新增抽象方法、参数 int → long、常量值改变
1
K10 在 2.0 上运行输出的常量值(重新编译后才是 2)

japicmp 的报告

实测 · 本机执行输出
  C1   状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 'old', 'REMOVED')]
  C2   状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 'size', 'MODIFIED')]
  C3   状态 MODIFIED  binaryCompatible=true  sourceCompatible=true  变化:[('method', 'b', 'NEW')]
  C6   状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 'm', 'MODIFIED')]
  C7   状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[]
  C9   状态 MODIFIED  binaryCompatible=true  sourceCompatible=false 变化:[('method', 'io', 'MODIFIED')]
  I4   状态 MODIFIED  binaryCompatible=true  sourceCompatible=false 变化:[('method', 'stop', 'NEW')]
  I5   状态 MODIFIED  binaryCompatible=true  sourceCompatible=true  变化:[('method', 'stop', 'NEW')]
  C11  状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 's', 'MODIFIED')]
  C12  状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('field', 'f', 'MODIFIED')]
  C14  状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 'm', 'REMOVED'), ('method', 'm', 'NEW')]
  C15  状态 REMOVED   binaryCompatible=false sourceCompatible=false 变化:[('constructor', 'C15', 'REMOVED'), ('method', 'x', 'REMOVED')]
  p.C8 状态 MODIFIED  binaryCompatible=false sourceCompatible=false 变化:[('method', 'hidden', 'MODIFIED')]
  --semantic-versioning --report-only-summary 的输出: ['1.0.0']
  常量 K 的实际行为:对 2.0 重新编译的 K10 输出 2 ;japicmp 的报告里 C10 没有出现任何变化

真实运行与重新编译

实测 · 本机执行输出
  客户端    在 1.0 上运行              换成 2.0 jar 运行                                                                                                   对 2.0 重新编译
  K1    2                      抛出 java.lang.NoSuchMethodError: 'int C1.old()'                                                                  编译失败:cannot find symbol   ← 删除方法 old()
  K2    1                      抛出 java.lang.NoSuchMethodError: 'int C2.size()'                                                                 编译失败:incompatible types: possible lossy conversion from long to int   ← 返回类型 int → long
  K3    1                      1                                                                                                               通过   ← 新增方法 b()
  K4    ok                     抛出 java.lang.AbstractMethodError: Receiver class K4$Impl does not define or inherit an implementation of the    编译失败:Impl is not abstract and does not override abstract method stop() in I   ← 接口新增抽象方法 stop()
  K5    ok                     ok                                                                                                              通过   ← 接口新增 default 方法 stop()
  K6    5                      抛出 java.lang.IncompatibleClassChangeError: class K6$Sub overrides final method C6.m()I                          编译失败:m() in Sub cannot override m() in C6   ← 方法变 final(客户端子类覆盖它)
  K7    1                      抛出 java.lang.IncompatibleClassChangeError: class K7$Sub cannot inherit from final class C7                      编译失败:cannot inherit from final C7   ← 类变 final(客户端子类继承它)
  K8    1                      抛出 java.lang.IllegalAccessError: class K8 tried to access method 'int p.C8.hidden()' (K8 and p.C8 are in unn    编译失败:hidden() is not public in C8; cannot be accessed from outside package   ← public 方法变包级私有
  K9    ok                     ok                                                                                                              编译失败:unreported exception IOException; must be caught or declared to be thr   ← 方法新增 throws IOException
  K10   1                      1                                                                                                               通过   ← 常量 K 从 1 改为 2
  K11   1                      抛出 java.lang.IncompatibleClassChangeError: Expected static method 'int C11.s()'                                 编译失败:non-static method s() cannot be referenced from a static context   ← static 方法变实例方法
  K12   7                      抛出 java.lang.NoSuchFieldError: Class C12 does not have member field 'int f'                                     编译失败:incompatible types: possible lossy conversion from long to int   ← 字段 int → long
  K13   1                      1                                                                                                               通过   ← 方法加 @Deprecated
  K14   3                      抛出 java.lang.NoSuchMethodError: 'int C14.m(int)'                                                                通过   ← 参数 int → long
  K15   1                      抛出 java.lang.NoClassDefFoundError: C15                                                                          编译失败:cannot find symbol   ← 整个类 C15 被删除

分步演示

Java API 兼容性
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;japicmp 0.23.1(命令行 japicmp.JApiCmp,--ignore-missing-classes -a public);15 个变化是我设计的最小例子,每个只验证单一变化
    • 没有验证:泛型签名变化(擦除后兼容但源码不兼容)、注解变化、模块 / 包级变化、serialVersionUID 变化对序列化兼容的影响、Revapi / Clirr 等其它工具、japicmp 的 --semantic-versioning 输出的含义(这次只输出了 1.0.0,没有解读)、Maven 插件的用法
    京ICP备2026067502号-1京公网安备11010502063487号