今天查了个与 OkHttp Maven 依赖相关的问题,本文做简单记录。起因是我们遇到一个 OkHttp 3.14.2 中存在的 bug,且此 bug 在新版本中已得到修复,于是我们将 OkHttp 升级到了最新的稳定版 4.10.0。且升级前还确认了 Upgrading to OkHttp 4 - OkHttp 中提到的不兼容的改动与该工程无关。升级后 CI 各个环境发布均正常,直到同事反馈本地工程启动报错:
1 | Caused by: java.lang.NoSuchFieldError: Companion |
类似的依赖问题处理过不少,于是看了下该问题,由于 OkHttp4 是由 Kotlin 编写,而我从未写过 Kotlin 的代码,对 Kotlin 也仅有耳闻,所以 Kotlin 的代码对我来说有一点陌生,不过关系不大。首先我们明确异常原因为字段不存在,且字段名为 Companion
,栈帧中最接近的一行为 Util.kt
的 70 行 okhttp/Util.kt:
1 | private val UNICODE_BOMS = Options.of( |
即 Options.of
这一行调用,查看 Util.kt
的导包语句,可知 Options
类的全限定类名为 okio.Options
,让我不得不怀疑异常与该类的加载有关,于是我在工程中搜索了下该类,确认 okio-1.12.0.jar
与 okio-jvm-3.0.0.jar
中同时存在全限定类名为 okio.Options
的类。而为何工程中同时存在这两个 jar 呢,我们可以使用 mvn dependency:tree
来验证,与 OkHttp 的相关依赖树如下:
1 | [INFO] | | +- com.squareup.okhttp3:okhttp:jar:4.10.0:compile |
可知有第三方 SDK 依赖了 OkHttp 2.7.5,而我们显式引入了 OkHttp 4.10.0,因为这两个 OkHttp 的 groupId 不同,所以被 Maven 视为不同的依赖,即项目中同时存在 OkHttp 2.7.5 与 OkHttp 4.10.0,且第三方 SDK 还依赖了 okio-1.12.0.jar,OkHttp 4.10.0 依赖了okio-jvm-3.0.0.jar,这两个 okio 的依赖因为 artifactId 不同被 Maven 视为不同的依赖。至此,我们知道项目中同时存在以上相关的 jar 包。
为了进一步验证该问题,我们在应用启动时加上 JVM 参数:-verbose:class
以观察类加载信息。可知在异常发生前的相关日志如下:
1 | [Loaded okhttp3.ConnectionPool from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar] |
可知,异常发生前最后一个加载的类为 okio.Options
,且是加载的 okio-1.12.0.jar 中的 okio.Options
,而按照依赖树,我们知道 OkHttp 4.10.0 理论上应该使用配对的 okio-jvm-3.0.0.jar 中的 okio.Options
,而关于为何加载到了 okio-1.12.0.jar 中的 okio.Options
,与 jar 的搜索顺序有关,可以参考:关于使用通配符时同一路径下 jar 的加载顺序,此处不再展开。回到 java.lang.NoSuchFieldError: Companion
这个异常,当加载到 okio-1.12.0.jar 中的 okio.Options
时,为何会触发该异常呢?我们看看 okio-1.12.0.jar 中的 okio.Options
源码 okio/Options.java at okio-parent-1.12.0:
1 | package okio; |
可知,在 okio-1.12.0.jar 中,okio.Options
源码由 Java 编写,且不存在名为 Companion
的字段,当访问到该字段的时候,则会触发 NoSuchFieldError,而是在哪里访问到了这个字段呢?在整个 okhttp/Util.kt 的源码中,只有 70 行这一处代码涉及 okio.Options
类,没有看到对 Companion
字段的显式访问,如果我们仔细观察异常的栈帧,不难发现 at okhttp3.internal.Util.<clinit>(Util.kt:70) ~[okhttp-4.10.0.jar:?]
中出错的是 okhttp3.internal.Util.<clinit>
调用,而 <clinit>
告诉我们这是一段静态初始化块,于是我将 Util.kt
对应的 Util.class
文件进行反编译,得到的部分 Java 代码如下:
1 | static { |
以上代码中的 5-7 行解决了我们的疑惑,即获取 Options
类中的静态实例 Companion
并调用该实例上的 of
方法,将返回值赋值给 UNICODE_BOMS
变量以完成 Util.kt
源码中第 70 行对 UNICODE_BOMS
变量的初始化操作。而 okhttp-4.10.0.jar 是基于 okio-jvm-3.0.0.jar 构建的,所以我们再看看 okio-jvm-3.0.0.jar 中 的 Options
类中的部分源码 okio/Options.kt at parent-3.0.0:
1 | package okio |
不难发现里面含有 companion object,且该 object 中存在静态方法 of
,但是我们并未发现以大写 C
开头的静态 Companion
字段,于是我将 Options.kt
对应的 Options.class
文件进行反编译,得到的部分 Java 代码如下:
1 | public final class Options extends AbstractList implements RandomAccess { |
可以看出 Kotlin 源码中 companion object 的实现机制为一个名为 Companion
的静态内部类,且在 Options
类中创建了一个 Companion
的静态实例,同时该实例名称与类名相同。且根据源码易知 Options.of
这个静态方法调用实际是调用的 Companion
实例的 of
方法。
至此,整个异常发生的原因已经分析完成,明确原因后解决方法总是不难,此处不再赘述。而关于该问题的根本原因,是由于 OkHttp 4.10.0 将 okio 依赖的 artifactId 改为了 okio-jvm,导致 okio 依赖不能使用 Maven 的仲裁机制选择其中一个 okio 实现,关于该问题可参考:okio as a dependency changed artifactId in 4.10.x release · Issue #7351 · square/okhttp · GitHub。
Reference
Kotlin Programming Language
Companion objects | Object expressions and declarations | Kotlin
Java: What is the difference between
okio as a dependency changed artifactId in 4.10.x release · Issue #7351 · square/okhttp · GitHub