bigywater.tech
Java · 432

Java API 变更报告

API 变更报告回答的是“升级这个库会不会破坏我的代码”。这篇构造两个版本的库,让旧 client 真实运行、重新编译,量出 japicmp 的预测哪里对、哪里错、哪些变化它根本看不到,再给出把检查接进构建时的退出码。

13 / 14
japicmp 的二进制兼容预测与旧 client 实际运行一致的场景数(接口新增抽象方法是例外)
0 条
commons-lang3 3.12.0 → 3.18.0 的二进制不兼容变更(源码不兼容 38 条)
B → A
枚举常量顺序 A,B,C 改成 C,A,B 后 Order.values()[1].name() 的变化,japicmp 没有任何报告

预测与真实运行对照

实测 · 本机执行输出
  S01 add(int,int) 改成 add(int,long):japicmp 二进制不兼容、源码不兼容(METHOD_ADDED_TO_PUBLIC_CLASS,METHOD_REMOVED);旧 client 在 v1:OK 3;在 v2:ERR NoSuchMethodError: 'int lib.Api.add(int, int)';针对 v2 重新编译:通过,运行 OK 3
  S02 删除 removed():japicmp 二进制不兼容、源码不兼容(METHOD_REMOVED);旧 client 在 v1:OK ok;在 v2:ERR NoSuchMethodError: 'void lib.Api.removed()';针对 v2 重新编译:编译失败:error: cannot find symbol
  S03 int ret() 改成 long ret():japicmp 二进制不兼容、源码不兼容(METHOD_RETURN_TYPE_CHANGED);旧 client 在 v1:OK 1;在 v2:ERR NoSuchMethodError: 'int lib.Api.ret()';针对 v2 重新编译:编译失败:error: incompatible types: possible lossy conversion from long to int
  S04 static int stat() 改成实例方法:japicmp 二进制不兼容、源码不兼容(METHOD_NO_LONGER_STATIC);旧 client 在 v1:OK 1;在 v2:ERR IncompatibleClassChangeError: Expected static method 'int lib.Api.stat()';针对 v2 重新编译:编译失败:error: non-static method stat() cannot be referenced from a static context
  S05 public secret() 改成 protected:japicmp 二进制不兼容、源码不兼容(METHOD_LESS_ACCESSIBLE);旧 client 在 v1:OK ok;在 v2:ERR IllegalAccessError: class S05 tried to access protected method 'void lib.Api.secret()' (S05 and lib.Api are in;针对 v2 重新编译:编译失败:error: secret() has protected access in Api
  S06 thr() 新增 throws Exception:japicmp 二进制兼容、源码不兼容(METHOD_NOW_THROWS_CHECKED_EXCEPTION);旧 client 在 v1:OK ok;在 v2:OK ok;针对 v2 重新编译:编译失败:error: unreported exception Exception; must be caught or declared to be thrown
  S07 Object get() 改成 String get():japicmp 二进制不兼容、源码不兼容(METHOD_RETURN_TYPE_CHANGED);旧 client 在 v1:OK x;在 v2:ERR NoSuchMethodError: 'java.lang.Object lib.Api.get()';针对 v2 重新编译:通过,运行 OK x
  S08 接口 Svc 新增抽象方法 b():japicmp 二进制兼容、源码不兼容(METHOD_ADDED_TO_INTERFACE);旧 client 在 v1:OK ok;在 v2:ERR AbstractMethodError: Receiver class S08$My does not define or inherit an implementation of the resolved method;针对 v2 重新编译:编译失败:error: My is not abstract and does not override abstract method b() in Svc
  S09 class Fin 改成 final:japicmp 二进制不兼容、源码不兼容(CLASS_NOW_FINAL);旧 client 在 v1:OK ok;在 v2:ERR IncompatibleClassChangeError: class S09$Sub cannot inherit from final class lib.Fin;针对 v2 重新编译:编译失败:error: cannot inherit from final Fin
  S10 删除枚举常量 RED:japicmp 二进制不兼容、源码不兼容(FIELD_REMOVED);旧 client 在 v1:OK RED;在 v2:ERR NoSuchFieldError: Class lib.Color does not have member field 'lib.Color RED';针对 v2 重新编译:编译失败:error: cannot find symbol
  S13 Child.m() 上移到父类 Base:japicmp 二进制兼容、源码兼容(无变更记录);旧 client 在 v1:OK m;在 v2:OK m;针对 v2 重新编译:通过,运行 OK m
  S14 class Abs 改成 abstract:japicmp 二进制不兼容、源码不兼容(CLASS_NOW_ABSTRACT);旧 client 在 v1:OK ok;在 v2:ERR InstantiationError: lib.Abs;针对 v2 重新编译:编译失败:error: Abs is abstract; cannot be instantiated
  S15 父类方法 fm() 改成 final(子类覆盖了它):japicmp 二进制不兼容、源码不兼容(METHOD_NOW_FINAL);旧 client 在 v1:OK sub;在 v2:ERR IncompatibleClassChangeError: class S15$Sub overrides final method lib.Base2.fm()Ljava/lang/String;;针对 v2 重新编译:编译失败:error: fm() in Sub cannot override fm() in Base2
  S17 对照:keep() 没有改,同类里新增了 extra():japicmp 二进制兼容、源码兼容(无变更记录);旧 client 在 v1:OK 7;在 v2:OK 7;针对 v2 重新编译:通过,运行 OK 7
  汇总:14 个场景里 japicmp 的二进制兼容预测与旧 client 运行结果一致 13 个;源码兼容预测与 javac 重新编译结果一致 12 个;二进制不兼容 10 个、源码不兼容 12 个、二进制兼容但源码不兼容 2 个、二进制不兼容但源码兼容 0 个

没有被报告的变化

实测 · 本机执行输出
  S11 枚举 Order 常量顺序 A,B,C 改成 C,A,B:japicmp 的变更记录 无(二进制兼容、源码兼容);旧 client 在 v1:OK B;在 v2:OK A;针对 v2 重新编译后在 v2:OK A
  S12 常量 K.MAX 由 10 改成 20:japicmp 的变更记录 无(二进制兼容、源码兼容);旧 client 在 v1:OK 10;在 v2:OK 10;针对 v2 重新编译后在 v2:OK 20
  日志里 K 与 Order 的状态:=== K;=== Order;=== Order;=== Order;=== Order;=== Order

真实库的变更报告

实测 · 本机执行输出
  类共 272 个:{'UNCHANGED': 166, 'NEW': 49, 'MODIFIED': 57};二进制不兼容的类 0 个;源码不兼容的类 10 个;二进制不兼容的变更条数 0、源码不兼容的变更条数 38
  源码不兼容变更的类型:METHOD_PARAMETER_GENERICS_CHANGED × 17;CLASS_GENERIC_TEMPLATE_CHANGED × 8;METHOD_RETURN_TYPE_GENERICS_CHANGED × 7;CLASS_GENERIC_TEMPLATE_GENERICS_CHANGED × 4;METHOD_NOW_THROWS_CHECKED_EXCEPTION × 1;METHOD_NO_LONGER_THROWS_CHECKED_EXCEPTION × 1
  源码不兼容的例子:DiffBuilder.append(METHOD_PARAMETER_GENERICS_CHANGED)、EventListenerSupport$ProxyInvocationHandler.invoke(METHOD_NOW_THROWS_CHECKED_EXCEPTION)、ExceptionUtils.rethrow(CLASS_GENERIC_TEMPLATE_CHANGED)、ObjectUtils.toString(CLASS_GENERIC_TEMPLATE_CHANGED);二进制不兼容的变更 0 条(旧 jar 仍能直接链接,但用新版本重新编译的源码可能要改)

接入构建的退出码

实测 · 本机执行输出
  v1 → v3(只新增了一个 public 方法):binary-incompatibility → rc=0;source-incompatibility → rc=0;modifications → rc=1
  v1 → v4(只给 thr() 新增 throws Exception):binary-incompatibility → rc=0;source-incompatibility → rc=1;modifications → rc=1
  v1 → v2(多种破坏性变更):binary-incompatibility → rc=1;source-incompatibility → rc=1;modifications → rc=1
  --semantic-versioning 建议的版本号变化:commons-lang3 3.12.0 → 3.18.0:0.1.0;v1 → v3(只新增方法):0.0.1;v1 → v4(只新增 throws):0.1.0;v1 → v2(多种破坏性变更):1.0.0;v1 → v1(没有变化):0.0.1(0.0.1 = 补丁、0.1.0 = 次版本、1.0.0 = 主版本)
  v1 → v2 加 -b 只输出二进制不兼容的类:5 个类行;stderr 最后一行:E: There is at least one incompatibility: lib.Abs:CLASS_NOW_ABSTRACT,lib.Api.add(int,int):METHOD_REMOVED,lib.Api.get():METHOD_RETURN_TYPE_CHANGED,lib.

分步演示

API 变更报告
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。japicmp 0.23.1(命令行,--xml-file 解析兼容性变更),JDK 21.0.12;lib v1 / v2 是自己写的小例子;commons-lang3 3.12.0 与 3.18.0 来自 Maven Central
    • 没有验证:Kotlin / Scala 库、模块化(module-info)导出变化、序列化兼容(serialVersionUID)的实际反序列化、反射与注解的兼容性、japicmp 的 Maven / Gradle 插件配置、revapi 与 jdeps 等其它工具的结果对比、传递依赖版本冲突
    京ICP备2026067502号-1京公网安备11010502063487号