Retrofit Java 测试模块的设计:Kotlin 依赖隔离与 multi-release jar 验证
【免费下载链接】retrofitA type-safe HTTP client for Android and the JVM项目地址: https://gitcode.com/gh_mirrors/re/retrofit
Retrofit 是一个面向 Android 与 JVM 的类型安全 HTTP 客户端。在它的工程结构中,retrofit/java-test/是一个看似"多余"却承担关键职责的独立测试模块:它既要把 Kotlin 等可选依赖彻底从测试环境中剔除,又要求测试运行在 multi-release jar(MRJAR)之上而非单纯的 classes 目录。本文以 retrofit/java-test/README.md 为骨架,结合仓库源码与 Gradle 构建配置,深入剖析这两个设计动机背后的实现细节,帮助你理解 Retrofit 如何保证核心库在纯 Java 环境下的可移植性与多 JDK 兼容性。
为什么需要独立的 java-test 模块
打开 retrofit/java-test/README.md,全文只有两句话,却直指工程上的两个痛点:
These are in a separate module for two reasons:
- It ensures optional dependencies (Kotlin stuff) are completely absent.
- It uses the multi-release jar on the classpath rather than only the classes folder.
翻译过来就是:Retrofit 把 Java 测试单独抽成一个模块,原因有二:
- 确保可选依赖(Kotlin 相关)在测试环境中完全不出现;
- 测试时使用的是 multi-release jar,而不是只编译出的 classes 文件夹。
从仓库根目录的 settings.gradle 可以看到,retrofit模块之下并列着android-test、java-test、kotlin-test、robovm-test、test-helpers五个测试相关模块。其中kotlin-test专门承载 Kotlin 扩展(suspend函数等)的测试,而java-test则要证明核心库在没有 Kotlin 的场景下依然正确。
动机一:彻底剔除 Kotlin 可选依赖
Retrofit 核心模块在 Gradle 中同时应用了java-library与org.jetbrains.kotlin.jvm插件(见 retrofit/build.gradle),并且对kotlinx-coroutines只是compileOnly声明(retrofit/build.gradle)。也就是说,Kotlin 运行时只是编译期可见的可选依赖。
问题在于:一旦测试与主模块共享同一个编译与运行环境,Kotlin 相关的类型和KotlinExtensions.kt(如suspend函数的挂起支持)就可能悄悄出现在 classpath 上,掩盖掉"纯 Java 消费者拿不到这些类"的真实情况。如果测试始终能看到 Kotlin,就永远无法验证核心库在完全没有 Kotlin 依赖的 JVM 上能否正常工作。
java-test模块正是为此而生。查看它的依赖声明 retrofit/java-test/build.gradle:
dependencies { testImplementation projects.retrofit testImplementation projects.retrofit.testHelpers testImplementation libs.findBugsAnnotations testImplementation libs.junit testImplementation libs.truth testImplementation libs.guava testImplementation libs.okhttp.mockwebserver }可以看到这里只有 JUnit、Truth、Guava 和 OkHttp MockWebServer 等纯 Java 测试设施,没有任何 Kotlin 相关依赖。配合test-helpers(位于 retrofit/test-helpers)提供ToStringConverterFactory等辅助转换器,java-test就构成了一个"最干净"的纯 Java 验证现场。
一个典型的佐证是 DefaultMethodsTest.java:它直接在纯 Java 接口上定义带@GET注解的抽象方法与调用它的default方法,并用 MockWebServer 断言两次调用都能拿到 "Hi" 响应。整个过程不涉及任何 Kotlin 类型。
动机二:在 multi-release jar 上运行测试
第二个动机更隐蔽,却直指 Retrofit 的版本兼容设计。
什么是 multi-release jar(MRJAR)
Multi-release jar 是 JDK 9 引入的机制:同一个 jar 包内,可以在META-INF/versions/<version>/目录下存放针对特定 JDK 版本的类文件,运行时 JVM 自动按自身版本挑选合适的类。Retrofit 利用这一点,为接口 default 方法的调用准备了三个版本的实现,全部同名DefaultMethodSupport:
| JDK 版本 | 实现方式 | 源码位置 |
|---|---|---|
| Java 8 – 13 | 反射构造受信任的MethodHandles.Lookup | retrofit/src/main/java/retrofit2/DefaultMethodSupport.java |
| Java 14 – 15 | 普通MethodHandles.lookup()即可(JDK-8209005) | retrofit/src/main/java14/retrofit2/DefaultMethodSupport.java |
| Java 16+ | 官方公开 APIInvocationHandler.invokeDefault | retrofit/src/main/java16/retrofit2/DefaultMethodSupport.java |
三个实现暴露相同的静态入口DefaultMethodSupport.invoke(method, declaringClass, proxy, args),被 Reflection.java 与 HttpServiceMethod.java 中的 proxy 回调调用,用于在运行时调度接口上的 default 方法。
MRJAR 的构建方式
构建侧的支撑在 retrofit/build.gradle。addMultiReleaseSourceSet(int version)会为每个目标 JDK 版本创建独立 source set:
def addMultiReleaseSourceSet(int version) { def sourceSet = sourceSets.create("java$version") sourceSet.java.srcDir("src/main/java$version") // 让该版本 source set 能看到主 source set 的类型 dependencies.add("java${version}Implementation", sourceSets.getByName("main").output) tasks.named("compileJava${version}Java", JavaCompile) { javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(version) vendor = JvmVendorSpec.AZUL } } tasks.named('jar', Jar) { from(sourceSet.output) { into("META-INF/versions/$version") } } } addMultiReleaseSourceSet(14) addMultiReleaseSourceSet(16)关键点:
src/main/java14与src/main/java16的源码会被编译进 jar 的META-INF/versions/14与META-INF/versions/16;- 编译器通过 toolchain 指定 AZUL 供应商的对应 JDK,保证字节码目标版本正确;
- jar 的 manifest 中声明
Multi-Release: true(retrofit/build.gradle),并附带Automatic-Module-Name: retrofit2。
为什么必须用 jar 而不是 classes 目录
这是 README 第二句话的真正含义。Gradle 测试任务默认把模块的classes输出目录直接放进 classpath。类目录(folder)没有META-INF/versions概念,运行时 JVM 只会加载根目录下的类——也就是 Java 8 版本的DefaultMethodSupport,无论测试跑在哪个 JDK 上。
只有当测试 classpath 上放的是打好包的 jar(包含META-INF/versions布局与 manifest 标记)时,JVM 才会按运行时的 JDK 版本选择java14或java16变体。java-test通过testImplementation projects.retrofit拿到的正是这个产物,从而真正验证了多版本分发逻辑在各自 JDK 上的表现。
这带来的一个实际意义,正如 CHANGELOG.md 所述:jar 中会包含目标为 Java 14/Java 16 字节码的类,一些不了解 MRJAR 的静态分析工具可能因此报错——而java-test恰好就是用来确认这类"不同 JDK 下运行时行为"的正确性的。
跨 JDK 测试矩阵:JDK 8 到 24 全覆盖
java-test不仅验证 MRJAR,还通过 Gradle 的 Java Toolchain 构建了完整的跨 JDK 测试矩阵。见 retrofit/java-test/build.gradle:
// Create a test task for each supported JDK. (8..24).each { majorVersion -> def jdkTest = tasks.register("testJdk$majorVersion", Test) { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(majorVersion) vendor = JvmVendorSpec.AZUL } // Wire directly to the test source set outputs for proper task dependencies. testClassesDirs = sourceSets.test.output.classesDirs classpath = sourceSets.test.runtimeClasspath } tasks.named("check").configure { dependsOn(jdkTest) } } // We don't need the built-in test task which uses Gradle's JVM given the above variants. tasks.getByName('test').enabled = false要点拆解:
- 对 JDK 8 到 JDK 24 的每一个版本注册一个独立的
Test任务testJdkX,通过 toolchain 拉取对应版本的 AZUL JDK 作为 launcher; - 所有任务都被挂到
check上,运行./gradlew :retrofit:java-test:check即会依次在全部 JDK 上执行测试; - 默认的
test任务被显式禁用,避免与这套矩阵任务重复执行。
这套矩阵的意义在于:Retrofit 声称兼容的最低版本是 Java 8,而DefaultMethodSupport在 Java 14、Java 16 各有行为差异(详见上文表格),只有在每个 JDK 版本上都真正运行一次测试,才能确认 MRJAR 的版本路由在运行时没有问题。
纯 Java 视角下的测试重点
既然该模块刻意排除了 Kotlin,它的测试内容也就集中于纯 Java API 行为。除了上面提到的 default 方法调用,java-test还覆盖了核心 API 的方方面面,例如:
- CallTest.java 与 ResponseTest.java:同步/异步调用与响应模型;
- RequestFactoryTest.java 与 RequestFactoryBuilderTest.java:
@GET、@Query等注解到请求的解析; - RetrofitTest.java 与 CallAdapterTest.java:
Retrofit.Builder与调用适配器机制; - HttpExceptionTest.java 与 InvocationTest.java:错误映射与调用元信息;
- Java8DefaultStaticMethodsInValidationTest.java:在
validateEagerly(true)下验证接口同时含 default 与 static 方法时,retrofit.create(...)依然能通过校验。
其中与 MRJAR 最直接相关的就是 default 方法测试。DefaultMethodsTest.java 定义了如下接口:
interface Example { @GET("/") Call<String> user(@Query("name") String name); default Call<String> user() { return user("hey"); } }user()这个 default 方法会转发到带注解的抽象方法上。当retrofit.create(Example.class)生成动态代理后,对user()的调用就必须经由DefaultMethodSupport.invoke走 method handle 链路。因此,这个测试在 JDK 8 上验证的是反射构造受信任Lookup的路径,在 JDK 14/16 上验证的则是各自的简化实现——一次测试,同时覆盖三个DefaultMethodSupport变体。
总结
retrofit/java-test模块虽小,却是 Retrofit 工程质量的关键一环。它用独立的模块边界保证了纯 Java 场景下 Kotlin 可选依赖的完全缺席,又借助 Gradle toolchain 与 MRJAR 机制,让测试在打包后的 multi-release jar上横跨 JDK 8 到 24 全矩阵运行,从而真实验证了DefaultMethodSupport在三代 JVM 实现间的版本路由。对于想要深入理解 Retrofit 构建架构、或在自己的多版本库中复刻这套"隔离测试 + MRJAR 验证"模式的开发者,retrofit/java-test/build.gradle 与 retrofit/build.gradle 就是现成的最佳范本。
【免费下载链接】retrofitA type-safe HTTP client for Android and the JVM项目地址: https://gitcode.com/gh_mirrors/re/retrofit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考