Skip to content

Commit 520eeef

Browse files
shanshinqwwdfsad
andauthored
Fixed serializers caching for parametrized types from different class loaders
Fixes #2065 PR #2070 Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
1 parent 77c8232 commit 520eeef

File tree

5 files changed

+162
-2
lines changed

5 files changed

+162
-2
lines changed

core/jvmMain/src/kotlinx/serialization/internal/Caching.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package kotlinx.serialization.internal
77
import kotlinx.serialization.KSerializer
88
import java.util.concurrent.ConcurrentHashMap
99
import kotlin.reflect.KClass
10+
import kotlin.reflect.KClassifier
1011
import kotlin.reflect.KType
12+
import kotlin.reflect.KTypeProjection
1113

1214
/*
1315
* By default, we use ClassValue-based caches to avoid classloader leaks,
@@ -101,10 +103,49 @@ private class ConcurrentHashMapParametrizedCache<T>(private val compute: (KClass
101103

102104
private class CacheEntry<T>(@JvmField val serializer: KSerializer<T>?)
103105

106+
/**
107+
* Workaround of https://youtrack.jetbrains.com/issue/KT-54611 and https://github.com/Kotlin/kotlinx.serialization/issues/2065
108+
*/
109+
private class KTypeWrapper(private val origin: KType) : KType {
110+
override val annotations: List<Annotation>
111+
get() = origin.annotations
112+
override val arguments: List<KTypeProjection>
113+
get() = origin.arguments
114+
override val classifier: KClassifier?
115+
get() = origin.classifier
116+
override val isMarkedNullable: Boolean
117+
get() = origin.isMarkedNullable
118+
119+
override fun equals(other: Any?): Boolean {
120+
if (other == null) return false
121+
if (origin != other) return false
122+
123+
val kClassifier = classifier
124+
if (kClassifier is KClass<*>) {
125+
val otherClassifier = (other as? KType)?.classifier
126+
if (otherClassifier == null || otherClassifier !is KClass<*>) {
127+
return false
128+
}
129+
return kClassifier.java == otherClassifier.java
130+
} else {
131+
return false
132+
}
133+
}
134+
135+
override fun hashCode(): Int {
136+
return origin.hashCode()
137+
}
138+
139+
override fun toString(): String {
140+
return "KTypeWrapper: $origin"
141+
}
142+
}
143+
104144
private class ParametrizedCacheEntry<T> {
105-
private val serializers: ConcurrentHashMap<List<KType>, Result<KSerializer<T>?>> = ConcurrentHashMap()
145+
private val serializers: ConcurrentHashMap<List<KTypeWrapper>, Result<KSerializer<T>?>> = ConcurrentHashMap()
106146
inline fun computeIfAbsent(types: List<KType>, producer: () -> KSerializer<T>?): Result<KSerializer<T>?> {
107-
return serializers.getOrPut(types) {
147+
val wrappedTypes = types.map { KTypeWrapper(it) }
148+
return serializers.getOrPut(wrappedTypes) {
108149
kotlin.runCatching { producer() }
109150
}
110151
}
Binary file not shown.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization
6+
7+
import kotlinx.serialization.json.Json
8+
import java.net.URLClassLoader
9+
import kotlin.reflect.*
10+
import kotlin.test.*
11+
12+
class SerializerByTypeCacheTest {
13+
14+
@Serializable
15+
class Holder(val i: Int)
16+
17+
@Suppress("UNCHECKED_CAST")
18+
@Test
19+
fun testCaching() {
20+
val typeOfKType = typeOf<Holder>()
21+
val parameterKType = typeOf<List<Holder>>().arguments[0].type!!
22+
assertSame(serializer(), serializer<Holder>())
23+
assertSame(serializer(typeOfKType), serializer(typeOfKType))
24+
assertSame(serializer(parameterKType), serializer(parameterKType))
25+
assertSame(serializer(), serializer(typeOfKType) as KSerializer<Holder>)
26+
assertSame(serializer(parameterKType) as KSerializer<Holder>, serializer(typeOfKType) as KSerializer<Holder>)
27+
}
28+
29+
/**
30+
* Checking the case when a parameterized type is loaded in different parallel [ClassLoader]s.
31+
*
32+
* If the main type is loaded by a common parent [ClassLoader] (for example, a bootstrap for [List]),
33+
* and the element class is loaded by different loaders, then some implementations of the [KType] (e.g. `KTypeImpl` from reflection) may not see the difference between them.
34+
*
35+
* As a result, a serializer for another loader will be returned from the cache, and it will generate instances, when working with which we will get an [ClassCastException].
36+
*
37+
* The test checks the correctness of the cache for such cases - that different serializers for different loaders will be returned.
38+
*
39+
* [see](https://youtrack.jetbrains.com/issue/KT-54523).
40+
*/
41+
@Test
42+
fun testDifferentClassLoaders() {
43+
val elementKType1 = SimpleKType(loadClass().kotlin)
44+
val elementKType2 = SimpleKType(loadClass().kotlin)
45+
46+
// Java class must be same (same name)
47+
assertEquals(elementKType1.classifier.java.canonicalName, elementKType2.classifier.java.canonicalName)
48+
// class loaders must be different
49+
assertNotSame(elementKType1.classifier.java.classLoader, elementKType2.classifier.java.classLoader)
50+
// due to the incorrect definition of the `equals`, KType-s are equal
51+
assertEquals(elementKType1, elementKType2)
52+
53+
// create parametrized type `List<Foo>`
54+
val kType1 = SingleParametrizedKType(List::class, elementKType1)
55+
val kType2 = SingleParametrizedKType(List::class, elementKType2)
56+
57+
val serializer1 = serializer(kType1)
58+
val serializer2 = serializer(kType2)
59+
60+
// when taking a serializers from cache, we must distinguish between KType-s, despite the fact that they are equivalent
61+
assertNotSame(serializer1, serializer2)
62+
63+
// serializers must work correctly
64+
Json.decodeFromString(serializer1, "[{\"i\":1}]")
65+
Json.decodeFromString(serializer2, "[{\"i\":1}]")
66+
}
67+
68+
/**
69+
* Load class `example.Foo` via new class loader. Compiled class-file located in the resources.
70+
*/
71+
private fun loadClass(): Class<*> {
72+
val classesUrl = this::class.java.classLoader.getResource("class_loaders/classes/")
73+
val loader1 = URLClassLoader(arrayOf(classesUrl), this::class.java.classLoader)
74+
return loader1.loadClass("example.Foo")
75+
}
76+
77+
private class SimpleKType(override val classifier: KClass<*>): KType {
78+
override val annotations: List<Annotation> = emptyList()
79+
override val arguments: List<KTypeProjection> = emptyList()
80+
81+
override val isMarkedNullable: Boolean = false
82+
83+
override fun equals(other: Any?): Boolean {
84+
if (other !is SimpleKType) return false
85+
return classifier.java.canonicalName == other.classifier.java.canonicalName
86+
}
87+
88+
override fun hashCode(): Int {
89+
return classifier.java.canonicalName.hashCode()
90+
}
91+
}
92+
93+
94+
private class SingleParametrizedKType(override val classifier: KClass<*>, val parameterType: KType): KType {
95+
override val annotations: List<Annotation> = emptyList()
96+
97+
override val arguments: List<KTypeProjection> = listOf(KTypeProjection(KVariance.INVARIANT, parameterType))
98+
99+
override val isMarkedNullable: Boolean = false
100+
101+
override fun equals(other: Any?): Boolean {
102+
if (this === other) return true
103+
if (javaClass != other?.javaClass) return false
104+
105+
other as SingleParametrizedKType
106+
107+
if (classifier != other.classifier) return false
108+
if (parameterType != other.parameterType) return false
109+
110+
return true
111+
}
112+
113+
override fun hashCode(): Int {
114+
var result = classifier.hashCode()
115+
result = 31 * result + parameterType.hashCode()
116+
return result
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)