Poison

java.lang.NoSuchFieldError: Companion

今天查了个与 OkHttp Maven 依赖相关的问题,本文做简单记录。起因是我们遇到一个 OkHttp 3.14.2 中存在的 bug,且此 bug 在新版本中已得到修复,于是我们将 OkHttp 升级到了最新的稳定版 4.10.0。且升级前还确认了 Upgrading to OkHttp 4 - OkHttp 中提到的不兼容的改动与该工程无关。升级后 CI 各个环境发布均正常,直到同事反馈本地工程启动报错:

1
2
3
4
5
Caused by: java.lang.NoSuchFieldError: Companion
at okhttp3.internal.Util.<clinit>(Util.kt:70) ~[okhttp-4.10.0.jar:?]
at okhttp3.internal.concurrent.TaskRunner.<clinit>(TaskRunner.kt:309) ~[okhttp-4.10.0.jar:?]
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:41) ~[okhttp-4.10.0.jar:?]
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:47) ~[okhttp-4.10.0.jar:?]

类似的依赖问题处理过不少,于是看了下该问题,由于 OkHttp4 是由 Kotlin 编写,而我从未写过 Kotlin 的代码,对 Kotlin 也仅有耳闻,所以 Kotlin 的代码对我来说有一点陌生,不过关系不大。首先我们明确异常原因为字段不存在,且字段名为 Companion,栈帧中最接近的一行为 Util.kt 的 70 行 okhttp/Util.kt:

1
2
3
4
5
6
7
private val UNICODE_BOMS = Options.of(
"efbbbf".decodeHex(), // UTF-8
"feff".decodeHex(), // UTF-16BE
"fffe".decodeHex(), // UTF-16LE
"0000ffff".decodeHex(), // UTF-32BE
"ffff0000".decodeHex() // UTF-32LE
)

Options.of 这一行调用,查看 Util.kt 的导包语句,可知 Options 类的全限定类名为 okio.Options,让我不得不怀疑异常与该类的加载有关,于是我在工程中搜索了下该类,确认 okio-1.12.0.jarokio-jvm-3.0.0.jar 中同时存在全限定类名为 okio.Options 的类。而为何工程中同时存在这两个 jar 呢,我们可以使用 mvn dependency:tree 来验证,与 OkHttp 的相关依赖树如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[INFO] |  |  +- com.squareup.okhttp3:okhttp:jar:4.10.0:compile
[INFO] | | | +- com.squareup.okio:okio-jvm:jar:3.0.0:compile
[INFO] | | | | +- org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.5.31:compile
[INFO] | | | | | \- org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.5.31:compile
[INFO] | | | | \- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.5.31:compile
[INFO] | | | \- org.jetbrains.kotlin:kotlin-stdlib:jar:1.6.20:compile
[INFO] | | | \- org.jetbrains:annotations:jar:13.0:compile

[INFO] | | +- me.tianshuang:sdk-java:jar:1.1:compile
[INFO] | | | \- me.tianshuang:sdk-java-common:jar:1.1:compile
[INFO] | | | +- com.squareup.okio:okio:jar:1.12.0:compile
[INFO] | | | +- com.squareup.okhttp:okhttp:jar:2.7.5:compile
[INFO] | | | +- com.squareup.okhttp:logging-interceptor:jar:2.7.5:compile
[INFO] | | | \- org.ini4j:ini4j:jar:0.5.4: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[Loaded okhttp3.ConnectionPool from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded kotlin.jvm.internal.Intrinsics from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/kotlin-stdlib-1.6.20.jar]
[Loaded kotlin.UninitializedPropertyAccessException from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/kotlin-stdlib-1.6.20.jar]
[Loaded kotlin.KotlinNullPointerException from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/kotlin-stdlib-1.6.20.jar]
[Loaded okhttp3.internal.connection.RealConnectionPool from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.connection.RealConnectionPool$Companion from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.concurrent.TaskRunner from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.concurrent.TaskRunner$Companion from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.concurrent.TaskRunner$Backend from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.concurrent.TaskRunner$RealBackend from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.internal.Util from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded kotlin.jvm.internal.markers.KMappedMarker from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/kotlin-stdlib-1.6.20.jar]
[Loaded okhttp3.Headers from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.Headers$Companion from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded kotlin.internal.ProgressionUtilKt from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/kotlin-stdlib-1.6.20.jar]
[Loaded okhttp3.ResponseBody from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.ResponseBody$Companion from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okio.Source from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.BufferedSource from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.Sink from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.BufferedSink from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.Buffer from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.Buffer$2 from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.ByteString from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.SegmentedByteString from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.Buffer$1 from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okio.Util from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]
[Loaded okhttp3.ResponseBody$Companion$asResponseBody$1 from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.RequestBody from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.RequestBody$Companion from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okhttp3.RequestBody$Companion$toRequestBody$2 from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okhttp-4.10.0.jar]
[Loaded okio.Options from file:/Users/tianshuang/IdeaProjects/sample/target/ROOT/WEB-INF/lib/okio-1.12.0.jar]

Caused by: java.lang.NoSuchFieldError: Companion
at okhttp3.internal.Util.<clinit>(Util.kt:70) ~[okhttp-4.10.0.jar:?]
at okhttp3.internal.concurrent.TaskRunner.<clinit>(TaskRunner.kt:309) ~[okhttp-4.10.0.jar:?]
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:41) ~[okhttp-4.10.0.jar:?]
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:47) ~[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package okio;

import java.util.AbstractList;
import java.util.RandomAccess;

/** An indexed set of values that may be read with {@link BufferedSource#select}. */
public final class Options extends AbstractList<ByteString> implements RandomAccess {
final ByteString[] byteStrings;

private Options(ByteString[] byteStrings) {
this.byteStrings = byteStrings;
}

public static Options of(ByteString... byteStrings) {
return new Options(byteStrings.clone()); // Defensive copy.
}

@Override public ByteString get(int i) {
return byteStrings[i];
}

@Override public int size() {
return byteStrings.length;
}
}

可知,在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static {
EMPTY_HEADERS = Headers.Companion.of(new String[0]);
EMPTY_RESPONSE = Companion.create$default(ResponseBody.Companion, EMPTY_BYTE_ARRAY, (MediaType)null, 1, (Object)null);
EMPTY_REQUEST = okhttp3.RequestBody.Companion.create$default(RequestBody.Companion, EMPTY_BYTE_ARRAY, (MediaType)null, 0, 0, 7, (Object)null);
Options.Companion var10000 = Options.Companion;
ByteString[] var0 = new ByteString[]{ByteString.Companion.decodeHex("efbbbf"), ByteString.Companion.decodeHex("feff"), ByteString.Companion.decodeHex("fffe"), ByteString.Companion.decodeHex("0000ffff"), ByteString.Companion.decodeHex("ffff0000")};
UNICODE_BOMS = var10000.of(var0);
TimeZone var1 = TimeZone.getTimeZone("GMT");
Intrinsics.checkNotNull(var1);
UTC = var1;
VERIFY_AS_IP_ADDRESS = new Regex("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
assertionsEnabled = OkHttpClient.class.desiredAssertionStatus();
String var2 = OkHttpClient.class.getName();
Intrinsics.checkNotNullExpressionValue(var2, "OkHttpClient::class.java.name");
okHttpName = StringsKt.removeSuffix(StringsKt.removePrefix(var2, (CharSequence)"okhttp3."), (CharSequence)"Client");
}

以上代码中的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package okio

import kotlin.jvm.JvmStatic

/** An indexed set of values that may be read with [BufferedSource.select]. */
class Options private constructor(
internal val byteStrings: Array<out ByteString>,
internal val trie: IntArray
) : AbstractList<ByteString>(), RandomAccess {

override val size: Int
get() = byteStrings.size

override fun get(index: Int) = byteStrings[index]

companion object {
@JvmStatic
fun of(vararg byteStrings: ByteString): Options {
if (byteStrings.isEmpty()) {
// With no choices we must always return -1. Create a trie that selects from an empty set.
return Options(arrayOf(), intArrayOf(0, -1))
}

// Sort the byte strings which is required when recursively building the trie. Map the sorted
// indexes to the caller's indexes.
val list = byteStrings.toMutableList()
list.sort()
val indexes = mutableListOf(*byteStrings.map { -1 }.toTypedArray())
byteStrings.forEachIndexed { callerIndex, byteString ->
val sortedIndex = list.binarySearch(byteString)
indexes[sortedIndex] = callerIndex
}
require(list[0].size > 0) { "the empty byte string is not a supported option" }

// Strip elements that will never be returned because they follow their own prefixes. For
// example, if the caller provides ["abc", "abcde"] we will never return "abcde" because we
// return as soon as we encounter "abc".
var a = 0
while (a < list.size) {
val prefix = list[a]
var b = a + 1
while (b < list.size) {
val byteString = list[b]
if (!byteString.startsWith(prefix)) break
require(byteString.size != prefix.size) { "duplicate option: $byteString" }
if (indexes[b] > indexes[a]) {
list.removeAt(b)
indexes.removeAt(b)
} else {
b++
}
}
a++
}

val trieBytes = Buffer()
buildTrieRecursive(node = trieBytes, byteStrings = list, indexes = indexes)

val trie = IntArray(trieBytes.intCount.toInt())
var i = 0
while (!trieBytes.exhausted()) {
trie[i++] = trieBytes.readInt()
}

return Options(byteStrings.copyOf() /* Defensive copy. */, trie)
}

private val Buffer.intCount get() = size / 4
}
}

不难发现里面含有 companion object,且该 object 中存在静态方法 of,但是我们并未发现以大写 C 开头的静态 Companion 字段,于是我将 Options.kt 对应的 Options.class 文件进行反编译,得到的部分 Java 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
public final class Options extends AbstractList implements RandomAccess {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

@JvmStatic
@NotNull
public static final Options of(@NotNull ByteString... byteStrings) {
return Companion.of(byteStrings);
}

@Metadata(
mv = {1, 5, 1},
k = 1,
xi = 48,
d1 = {"\u0000>\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\t\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0002\b\u0003\n\u0002\u0010\b\n\u0000\n\u0002\u0010 \n\u0002\u0018\u0002\n\u0002\b\u0004\n\u0002\u0018\u0002\n\u0002\u0010\u0011\n\u0002\b\u0002\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002JT\u0010\b\u001a\u00020\t2\b\b\u0002\u0010\n\u001a\u00020\u00042\u0006\u0010\u000b\u001a\u00020\u00052\b\b\u0002\u0010\f\u001a\u00020\r2\f\u0010\u000e\u001a\b\u0012\u0004\u0012\u00020\u00100\u000f2\b\b\u0002\u0010\u0011\u001a\u00020\r2\b\b\u0002\u0010\u0012\u001a\u00020\r2\f\u0010\u0013\u001a\b\u0012\u0004\u0012\u00020\r0\u000fH\u0002J!\u0010\u0014\u001a\u00020\u00152\u0012\u0010\u000e\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00100\u0016\"\u00020\u0010H\u0007¢\u0006\u0002\u0010\u0017R\u0018\u0010\u0003\u001a\u00020\u0004*\u00020\u00058BX\u0082\u0004¢\u0006\u0006\u001a\u0004\b\u0006\u0010\u0007¨\u0006\u0018"},
d2 = {"Lokio/Options$Companion;", "", "()V", "intCount", "", "Lokio/Buffer;", "getIntCount", "(Lokio/Buffer;)J", "buildTrieRecursive", "", "nodeOffset", "node", "byteStringOffset", "", "byteStrings", "", "Lokio/ByteString;", "fromIndex", "toIndex", "indexes", "of", "Lokio/Options;", "", "([Lokio/ByteString;)Lokio/Options;", "okio"}
)
public static final class Companion {
private Companion() {
}

@JvmStatic
@NotNull
public final Options of(@NotNull ByteString... byteStrings) {
Intrinsics.checkNotNullParameter(byteStrings, "byteStrings");
boolean var3 = false;
if (byteStrings.length == 0) {
ByteString[] var40 = new ByteString[0];
int[] var18 = new int[]{0, -1};
return new Options(var40, var18, (DefaultConstructorMarker)null);
} else {
List list = ArraysKt.toMutableList(byteStrings);
CollectionsKt.sort(list);
int $i$f$toTypedArray = false;
Collection destination$iv$iv = (Collection)(new ArrayList(byteStrings.length));
int $i$f$mapTo = false;
ByteString[] var10 = byteStrings;
int var11 = byteStrings.length;

int callerIndex;
for(callerIndex = 0; callerIndex < var11; ++callerIndex) {
Object item$iv$iv = var10[callerIndex];
int var15 = false;
Integer var17 = -1;
destination$iv$iv.add(var17);
}

Collection $this$toTypedArray$iv = (Collection)((List)destination$iv$iv);
$i$f$toTypedArray = false;
Object[] var10000 = $this$toTypedArray$iv.toArray(new Integer[0]);
if (var10000 == null) {
throw new NullPointerException("null cannot be cast to non-null type kotlin.Array<T>");
} else {
Integer[] var4 = (Integer[])var10000;
List indexes = CollectionsKt.mutableListOf(Arrays.copyOf(var4, var4.length));
int $i$f$forEachIndexed = false;
int b = 0;
ByteString[] var7 = byteStrings;
int var28 = byteStrings.length;

for(int var33 = 0; var33 < var28; ++var33) {
Object item$iv = var7[var33];
callerIndex = b++;
int var38 = false;
int sortedIndex = CollectionsKt.binarySearch$default(list, (Comparable)item$iv, 0, 0, 6, (Object)null);
indexes.set(sortedIndex, callerIndex);
}

boolean var20 = ((ByteString)list.get(0)).size() > 0;
$i$f$forEachIndexed = false;
$i$f$toTypedArray = false;
if (!var20) {
int var29 = false;
String var31 = "the empty byte string is not a supported option";
throw (Throwable)(new IllegalArgumentException(var31.toString()));
} else {
for(int a = 0; a < list.size(); ++a) {
ByteString prefix = (ByteString)list.get(a);
b = a + 1;

while(b < list.size()) {
ByteString byteString = (ByteString)list.get(b);
if (!byteString.startsWith(prefix)) {
break;
}

boolean var32 = byteString.size() != prefix.size();
$i$f$mapTo = false;
boolean var35 = false;
if (!var32) {
int var37 = false;
String var36 = Intrinsics.stringPlus("duplicate option: ", byteString);
throw (Throwable)(new IllegalArgumentException(var36.toString()));
}

if (((Number)indexes.get(b)).intValue() > ((Number)indexes.get(a)).intValue()) {
list.remove(b);
indexes.remove(b);
} else {
++b;
}
}
}

Buffer trieBytes = new Buffer();
buildTrieRecursive$default(this, 0L, trieBytes, 0, list, 0, 0, indexes, 53, (Object)null);
int[] trie = new int[(int)this.getIntCount(trieBytes)];

for(int i = 0; !trieBytes.exhausted(); trie[var28] = trieBytes.readInt()) {
var28 = i++;
}

$i$f$mapTo = false;
Object[] var10002 = Arrays.copyOf(byteStrings, byteStrings.length);
Intrinsics.checkNotNullExpressionValue(var10002, "java.util.Arrays.copyOf(this, size)");
return new Options((ByteString[])var10002, trie, (DefaultConstructorMarker)null);
}
}
}
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

可以看出 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 and ? - Stack Overflow
okio as a dependency changed artifactId in 4.10.x release · Issue #7351 · square/okhttp · GitHub