Skip to content

Commit dc51a9e

Browse files
authored
Merge pull request #135 from ProjectMapK/value-class-unbox-cache
Fix to reuse ValueClassUnboxConverter
2 parents fd559e7 + c8d4fd3 commit dc51a9e

File tree

8 files changed

+70
-52
lines changed

8 files changed

+70
-52
lines changed

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.github.projectmapk.jackson.module.kogera
22

3+
import com.fasterxml.jackson.databind.JavaType
34
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
5+
import com.fasterxml.jackson.databind.type.TypeFactory
46
import com.fasterxml.jackson.databind.util.StdConverter
57

68
/**
@@ -22,3 +24,18 @@ internal class ValueClassBoxConverter<S : Any?, D : Any>(
2224

2325
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
2426
}
27+
28+
internal class ValueClassUnboxConverter<T : Any>(val valueClass: Class<T>) : StdConverter<T, Any?>() {
29+
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
30+
if (!this.isAccessible) this.isAccessible = true
31+
}
32+
val unboxedClass: Class<*> = unboxMethod.returnType
33+
34+
override fun convert(value: T): Any? = unboxMethod.invoke(value)
35+
36+
override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
37+
override fun getOutputType(typeFactory: TypeFactory): JavaType =
38+
typeFactory.constructType(unboxMethod.genericReturnType)
39+
40+
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
41+
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/KotlinModule.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ public class KotlinModule private constructor(
102102

103103
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
104104
context.addKeyDeserializers(KotlinKeyDeserializers)
105-
context.addSerializers(KotlinSerializers())
106-
context.addKeySerializers(KotlinKeySerializers())
105+
context.addSerializers(KotlinSerializers(cache))
106+
context.addKeySerializers(KotlinKeySerializers(cache))
107107

108108
// ranges
109109
context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java)

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
99
companion object {
1010
// Increment is required when properties that use LRUMap are changed.
1111
@Suppress("ConstPropertyName")
12-
private const val serialVersionUID = 2L
12+
private const val serialVersionUID = 3L
1313
}
1414

1515
// This cache is used for both serialization and deserialization, so reserve a larger size from the start.
@@ -23,6 +23,8 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
2323
// since the cache is used only twice locally at initialization per property.
2424
private val valueClassBoxConverterCache: LRUMap<Class<*>, ValueClassBoxConverter<*, *>> =
2525
LRUMap(0, reflectionCacheSize)
26+
private val valueClassUnboxConverterCache: LRUMap<Class<*>, ValueClassUnboxConverter<*>> =
27+
LRUMap(0, reflectionCacheSize)
2628

2729
fun getJmClass(clazz: Class<*>): JmClass? {
2830
return classCache[clazz] ?: run {
@@ -75,4 +77,11 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
7577

7678
(valueClassBoxConverterCache.putIfAbsent(valueClass, value) ?: value)
7779
}
80+
81+
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> =
82+
valueClassUnboxConverterCache.get(valueClass) ?: run {
83+
val value = ValueClassUnboxConverter(valueClass)
84+
85+
(valueClassUnboxConverterCache.putIfAbsent(valueClass, value) ?: value)
86+
}
7887
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation_introspector/KotlinFallbackAnnotationIntrospector.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import com.fasterxml.jackson.databind.type.TypeFactory
1313
import com.fasterxml.jackson.databind.util.Converter
1414
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
1515
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
16+
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
1617
import io.github.projectmapk.jackson.module.kogera.deser.CollectionValueStrictNullChecksConverter
1718
import io.github.projectmapk.jackson.module.kogera.deser.MapValueStrictNullChecksConverter
18-
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassUnboxConverter
1919
import io.github.projectmapk.jackson.module.kogera.isNullable
2020
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
2121
import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/Converters.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ import io.github.projectmapk.jackson.module.kogera.JavaDuration
1010
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
1111
import kotlin.time.toKotlinDuration
1212

13-
internal class ValueClassUnboxConverter<T : Any>(private val valueClass: Class<T>) : StdConverter<T, Any?>() {
14-
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
15-
if (!this.isAccessible) this.isAccessible = true
16-
}
17-
18-
override fun convert(value: T): Any? = unboxMethod.invoke(value)
19-
20-
override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
21-
}
22-
2313
internal sealed class CollectionValueStrictNullChecksConverter<T : Any> : Converter<T, T> {
2414
protected abstract val type: JavaType
2515
protected abstract val paramName: String

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ internal class KotlinDeserializers(
148148
rawClass == KotlinDuration::class.java ->
149149
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
150150
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)?.let {
151-
val unboxedClass = rawClass.getMethod("unbox-impl").returnType
151+
val unboxedClass = cache.getValueClassUnboxConverter(rawClass).unboxedClass
152152
val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass)
153153
ValueClassBoxDeserializer(it, converter)
154154
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ import com.fasterxml.jackson.databind.SerializationConfig
99
import com.fasterxml.jackson.databind.SerializerProvider
1010
import com.fasterxml.jackson.databind.ser.Serializers
1111
import com.fasterxml.jackson.databind.ser.std.StdSerializer
12+
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
13+
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
1214
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
1315
import java.lang.reflect.Method
1416
import java.lang.reflect.Modifier
1517

16-
internal object ValueClassUnboxKeySerializer : StdSerializer<Any>(Any::class.java) {
17-
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
18-
val method = value::class.java.getMethod("unbox-impl")
19-
val unboxed = method.invoke(value)
18+
internal class ValueClassUnboxKeySerializer<T : Any>(
19+
private val converter: ValueClassUnboxConverter<T>
20+
) : StdSerializer<T>(converter.valueClass) {
21+
override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
22+
val unboxed = converter.convert(value)
2023

2124
if (unboxed == null) {
22-
val javaType = provider.typeFactory.constructType(method.genericReturnType)
25+
val javaType = converter.getOutputType(provider.typeFactory)
2326
provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider)
2427
return
2528
}
@@ -33,15 +36,14 @@ private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.fi
3336
Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value }
3437
}
3538

36-
internal class ValueClassStaticJsonKeySerializer<T>(
37-
t: Class<T>,
39+
internal class ValueClassStaticJsonKeySerializer<T : Any>(
40+
private val converter: ValueClassUnboxConverter<T>,
3841
private val staticJsonKeyGetter: Method
39-
) : StdSerializer<T>(t) {
42+
) : StdSerializer<T>(converter.valueClass) {
4043
private val keyType: Class<*> = staticJsonKeyGetter.returnType
41-
private val unboxMethod: Method = t.getMethod("unbox-impl")
4244

4345
override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
44-
val unboxed = unboxMethod.invoke(value)
46+
val unboxed = converter.convert(value)
4547
// As shown in the processing of the factory function, jsonValueGetter is always a static method.
4648
val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed)
4749

@@ -57,19 +59,26 @@ internal class ValueClassStaticJsonKeySerializer<T>(
5759
// If create a function with a JsonValue in the value class,
5860
// it will be compiled as a static method (= cannot be processed properly by Jackson),
5961
// so use a ValueClassSerializer.StaticJsonValue to handle this.
60-
fun createOrNull(t: Class<*>): StdSerializer<*>? =
61-
t.getStaticJsonKeyGetter()?.let { ValueClassStaticJsonKeySerializer(t, it) }
62+
fun <T : Any> createOrNull(converter: ValueClassUnboxConverter<T>): StdSerializer<T>? =
63+
converter.valueClass.getStaticJsonKeyGetter()?.let { ValueClassStaticJsonKeySerializer(converter, it) }
6264
}
6365
}
6466

65-
internal class KotlinKeySerializers : Serializers.Base() {
67+
internal class KotlinKeySerializers(private val cache: ReflectionCache) : Serializers.Base() {
6668
override fun findSerializer(
6769
config: SerializationConfig,
6870
type: JavaType,
6971
beanDesc: BeanDescription
70-
): JsonSerializer<*>? = when {
71-
type.rawClass.isUnboxableValueClass() -> ValueClassStaticJsonKeySerializer.createOrNull(type.rawClass)
72-
?: ValueClassUnboxKeySerializer
73-
else -> null
72+
): JsonSerializer<*>? {
73+
val rawClass = type.rawClass
74+
75+
return when {
76+
rawClass.isUnboxableValueClass() -> {
77+
val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
78+
ValueClassStaticJsonKeySerializer.createOrNull(unboxConverter)
79+
?: ValueClassUnboxKeySerializer(unboxConverter)
80+
}
81+
else -> null
82+
}
7483
}
7584
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.SerializationConfig
99
import com.fasterxml.jackson.databind.SerializerProvider
1010
import com.fasterxml.jackson.databind.ser.Serializers
1111
import com.fasterxml.jackson.databind.ser.std.StdSerializer
12+
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
13+
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
1214
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
1315
import java.lang.reflect.Method
1416
import java.lang.reflect.Modifier
@@ -39,28 +41,17 @@ internal object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
3941
}
4042
}
4143

42-
// In accordance with kotlinx.serialization,
43-
// value classes are unboxed and serialized if they do not have a specified serializer.
44-
internal object ValueClassUnboxSerializer : StdSerializer<Any>(Any::class.java) {
45-
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
46-
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
47-
provider.defaultSerializeValue(unboxed, gen)
48-
}
49-
}
50-
5144
// Class must be UnboxableValueClass.
5245
private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method ->
5346
Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value }
5447
}
5548

56-
internal class ValueClassStaticJsonValueSerializer<T>(
57-
t: Class<T>,
49+
internal class ValueClassStaticJsonValueSerializer<T : Any>(
50+
private val converter: ValueClassUnboxConverter<T>,
5851
private val staticJsonValueGetter: Method
59-
) : StdSerializer<T>(t) {
60-
private val unboxMethod: Method = t.getMethod("unbox-impl")
61-
52+
) : StdSerializer<T>(converter.valueClass) {
6253
override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
63-
val unboxed = unboxMethod.invoke(value)
54+
val unboxed = converter.convert(value)
6455
// As shown in the processing of the factory function, jsonValueGetter is always a static method.
6556
val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed)
6657
provider.defaultSerializeValue(jsonValue, gen)
@@ -71,12 +62,12 @@ internal class ValueClassStaticJsonValueSerializer<T>(
7162
// If create a function with a JsonValue in the value class,
7263
// it will be compiled as a static method (= cannot be processed properly by Jackson),
7364
// so use a ValueClassSerializer.StaticJsonValue to handle this.
74-
fun createOrNull(t: Class<*>): StdSerializer<*>? =
75-
t.getStaticJsonValueGetter()?.let { ValueClassStaticJsonValueSerializer(t, it) }
65+
fun <T : Any> createOrNull(converter: ValueClassUnboxConverter<T>): StdSerializer<T>? =
66+
converter.valueClass.getStaticJsonValueGetter()?.let { ValueClassStaticJsonValueSerializer(converter, it) }
7667
}
7768
}
7869

79-
internal class KotlinSerializers : Serializers.Base() {
70+
internal class KotlinSerializers(private val cache: ReflectionCache) : Serializers.Base() {
8071
override fun findSerializer(
8172
config: SerializationConfig?,
8273
type: JavaType,
@@ -90,8 +81,10 @@ internal class KotlinSerializers : Serializers.Base() {
9081
UInt::class.java == rawClass -> UIntSerializer
9182
ULong::class.java == rawClass -> ULongSerializer
9283
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
93-
rawClass.isUnboxableValueClass() -> ValueClassStaticJsonValueSerializer.createOrNull(rawClass)
94-
?: ValueClassUnboxSerializer
84+
rawClass.isUnboxableValueClass() -> {
85+
val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
86+
ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer
87+
}
9588
else -> null
9689
}
9790
}

0 commit comments

Comments
 (0)