Skip to content

Commit

Permalink
Fixed serializers caching for parametrized types from different class…
Browse files Browse the repository at this point in the history
… loaders

Fixes #2065
PR #2070

Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
  • Loading branch information
shanshin and qwwdfsad authored Nov 1, 2022
1 parent 77c8232 commit 520eeef
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 2 deletions.
45 changes: 43 additions & 2 deletions core/jvmMain/src/kotlinx/serialization/internal/Caching.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ package kotlinx.serialization.internal
import kotlinx.serialization.KSerializer
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection

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

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

/**
* Workaround of https://youtrack.jetbrains.com/issue/KT-54611 and https://github.com/Kotlin/kotlinx.serialization/issues/2065
*/
private class KTypeWrapper(private val origin: KType) : KType {
override val annotations: List<Annotation>
get() = origin.annotations
override val arguments: List<KTypeProjection>
get() = origin.arguments
override val classifier: KClassifier?
get() = origin.classifier
override val isMarkedNullable: Boolean
get() = origin.isMarkedNullable

override fun equals(other: Any?): Boolean {
if (other == null) return false
if (origin != other) return false

val kClassifier = classifier
if (kClassifier is KClass<*>) {
val otherClassifier = (other as? KType)?.classifier
if (otherClassifier == null || otherClassifier !is KClass<*>) {
return false
}
return kClassifier.java == otherClassifier.java
} else {
return false
}
}

override fun hashCode(): Int {
return origin.hashCode()
}

override fun toString(): String {
return "KTypeWrapper: $origin"
}
}

private class ParametrizedCacheEntry<T> {
private val serializers: ConcurrentHashMap<List<KType>, Result<KSerializer<T>?>> = ConcurrentHashMap()
private val serializers: ConcurrentHashMap<List<KTypeWrapper>, Result<KSerializer<T>?>> = ConcurrentHashMap()
inline fun computeIfAbsent(types: List<KType>, producer: () -> KSerializer<T>?): Result<KSerializer<T>?> {
return serializers.getOrPut(types) {
val wrappedTypes = types.map { KTypeWrapper(it) }
return serializers.getOrPut(wrappedTypes) {
kotlin.runCatching { producer() }
}
}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.json.Json
import java.net.URLClassLoader
import kotlin.reflect.*
import kotlin.test.*

class SerializerByTypeCacheTest {

@Serializable
class Holder(val i: Int)

@Suppress("UNCHECKED_CAST")
@Test
fun testCaching() {
val typeOfKType = typeOf<Holder>()
val parameterKType = typeOf<List<Holder>>().arguments[0].type!!
assertSame(serializer(), serializer<Holder>())
assertSame(serializer(typeOfKType), serializer(typeOfKType))
assertSame(serializer(parameterKType), serializer(parameterKType))
assertSame(serializer(), serializer(typeOfKType) as KSerializer<Holder>)
assertSame(serializer(parameterKType) as KSerializer<Holder>, serializer(typeOfKType) as KSerializer<Holder>)
}

/**
* Checking the case when a parameterized type is loaded in different parallel [ClassLoader]s.
*
* If the main type is loaded by a common parent [ClassLoader] (for example, a bootstrap for [List]),
* 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.
*
* 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].
*
* The test checks the correctness of the cache for such cases - that different serializers for different loaders will be returned.
*
* [see](https://youtrack.jetbrains.com/issue/KT-54523).
*/
@Test
fun testDifferentClassLoaders() {
val elementKType1 = SimpleKType(loadClass().kotlin)
val elementKType2 = SimpleKType(loadClass().kotlin)

// Java class must be same (same name)
assertEquals(elementKType1.classifier.java.canonicalName, elementKType2.classifier.java.canonicalName)
// class loaders must be different
assertNotSame(elementKType1.classifier.java.classLoader, elementKType2.classifier.java.classLoader)
// due to the incorrect definition of the `equals`, KType-s are equal
assertEquals(elementKType1, elementKType2)

// create parametrized type `List<Foo>`
val kType1 = SingleParametrizedKType(List::class, elementKType1)
val kType2 = SingleParametrizedKType(List::class, elementKType2)

val serializer1 = serializer(kType1)
val serializer2 = serializer(kType2)

// when taking a serializers from cache, we must distinguish between KType-s, despite the fact that they are equivalent
assertNotSame(serializer1, serializer2)

// serializers must work correctly
Json.decodeFromString(serializer1, "[{\"i\":1}]")
Json.decodeFromString(serializer2, "[{\"i\":1}]")
}

/**
* Load class `example.Foo` via new class loader. Compiled class-file located in the resources.
*/
private fun loadClass(): Class<*> {
val classesUrl = this::class.java.classLoader.getResource("class_loaders/classes/")
val loader1 = URLClassLoader(arrayOf(classesUrl), this::class.java.classLoader)
return loader1.loadClass("example.Foo")
}

private class SimpleKType(override val classifier: KClass<*>): KType {
override val annotations: List<Annotation> = emptyList()
override val arguments: List<KTypeProjection> = emptyList()

override val isMarkedNullable: Boolean = false

override fun equals(other: Any?): Boolean {
if (other !is SimpleKType) return false
return classifier.java.canonicalName == other.classifier.java.canonicalName
}

override fun hashCode(): Int {
return classifier.java.canonicalName.hashCode()
}
}


private class SingleParametrizedKType(override val classifier: KClass<*>, val parameterType: KType): KType {
override val annotations: List<Annotation> = emptyList()

override val arguments: List<KTypeProjection> = listOf(KTypeProjection(KVariance.INVARIANT, parameterType))

override val isMarkedNullable: Boolean = false

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as SingleParametrizedKType

if (classifier != other.classifier) return false
if (parameterType != other.parameterType) return false

return true
}

override fun hashCode(): Int {
var result = classifier.hashCode()
result = 31 * result + parameterType.hashCode()
return result
}
}
}

0 comments on commit 520eeef

Please sign in to comment.