From 9027a8a41269880a7d28cf25e167dc50c4236a22 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 21:31:51 +0900 Subject: [PATCH 01/12] Ported converters that uses MethodHandles from https://github.com/ProjectMapK/jackson-module-kogera/blob/1579484a6e57303cca065b6897a2ae88ac21ec0c/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt --- .../jackson/module/kotlin/Converters.kt | 173 ++++++++++++++++-- .../jackson/module/kotlin/InternalCommons.kt | 17 ++ .../module/kotlin/KotlinKeyDeserializers.kt | 2 +- .../jackson/module/kotlin/ReflectionCache.kt | 2 +- 4 files changed, 180 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt index c284fb8c3..7902b072e 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt @@ -4,9 +4,13 @@ import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer import com.fasterxml.jackson.databind.type.TypeFactory -import com.fasterxml.jackson.databind.util.ClassUtil import com.fasterxml.jackson.databind.util.StdConverter -import kotlin.reflect.KClass +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.lang.reflect.Method +import java.lang.reflect.Type +import java.util.UUID import kotlin.time.toJavaDuration import kotlin.time.toKotlinDuration import java.time.Duration as JavaDuration @@ -23,7 +27,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon } internal object KotlinDurationValueToJavaDurationConverter : StdConverter() { - private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class) } + private val boxConverter by lazy { LongValueClassBoxConverter(KotlinDuration::class.java) } override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value)) } @@ -45,18 +49,163 @@ internal object JavaToKotlinDurationConverter : StdConverter( - unboxedClass: Class, - val boxedClass: KClass -) : StdConverter() { - private val boxMethod = boxedClass.java.getDeclaredMethod("box-impl", unboxedClass).apply { - ClassUtil.checkAndFixAccess(this, false) +internal sealed class ValueClassBoxConverter : StdConverter() { + abstract val boxedClass: Class + abstract val boxHandle: MethodHandle + + protected fun rawBoxHandle( + unboxedClass: Class<*>, + ): MethodHandle = MethodHandles.lookup().findStatic( + boxedClass, + "box-impl", + MethodType.methodType(boxedClass, unboxedClass), + ) + + val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } + + companion object { + fun create( + unboxedClass: Class<*>, + valueClass: Class<*>, + ): ValueClassBoxConverter<*, *> = when (unboxedClass) { + Int::class.java -> IntValueClassBoxConverter(valueClass) + Long::class.java -> LongValueClassBoxConverter(valueClass) + String::class.java -> StringValueClassBoxConverter(valueClass) + UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass) + else -> GenericValueClassBoxConverter(unboxedClass, valueClass) + } } + // If the wrapped type is explicitly specified, it is inherited for the sake of distinction + internal sealed class Specified : ValueClassBoxConverter() +} + +// region: Converters for common classes as wrapped values, add as needed. +internal class IntValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + override val boxHandle: MethodHandle = rawBoxHandle(Int::class.java).asType(INT_TO_ANY_METHOD_TYPE) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: Int): D = boxHandle.invokeExact(value) as D +} + +internal class LongValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + override val boxHandle: MethodHandle = rawBoxHandle(Long::class.java).asType(LONG_TO_ANY_METHOD_TYPE) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: Long): D = boxHandle.invokeExact(value) as D +} + +internal class StringValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + override val boxHandle: MethodHandle = rawBoxHandle(String::class.java).asType(STRING_TO_ANY_METHOD_TYPE) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: String?): D = boxHandle.invokeExact(value) as D +} + +internal class JavaUuidValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + override val boxHandle: MethodHandle = rawBoxHandle(UUID::class.java).asType(JAVA_UUID_TO_ANY_METHOD_TYPE) + + @Suppress("UNCHECKED_CAST") + override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D +} +// endregion + +/** + * A converter that only performs box processing for the value class. + * Note that constructor-impl is not called. + * @param S is nullable because value corresponds to a nullable value class. + * see [io.github.projectmapk.jackson.module.kogera.annotationIntrospector.KotlinFallbackAnnotationIntrospector.findNullSerializer] + */ +internal class GenericValueClassBoxConverter( + unboxedClass: Class, + override val boxedClass: Class, +) : ValueClassBoxConverter() { + override val boxHandle: MethodHandle = rawBoxHandle(unboxedClass).asType(ANY_TO_ANY_METHOD_TYPE) + @Suppress("UNCHECKED_CAST") - override fun convert(value: S): D = boxMethod.invoke(null, value) as D + override fun convert(value: S): D = boxHandle.invokeExact(value) as D +} + +internal sealed class ValueClassUnboxConverter : StdConverter() { + abstract val valueClass: Class + abstract val unboxedType: Type + abstract val unboxHandle: MethodHandle + + final override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass) + final override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(unboxedType) val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) } + + companion object { + fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> { + val unboxMethod = valueClass.getDeclaredMethod("unbox-impl") + val unboxedType = unboxMethod.genericReturnType + + return when (unboxedType) { + Int::class.java -> IntValueClassUnboxConverter(valueClass, unboxMethod) + Long::class.java -> LongValueClassUnboxConverter(valueClass, unboxMethod) + String::class.java -> StringValueClassUnboxConverter(valueClass, unboxMethod) + UUID::class.java -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod) + else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod) + } + } + } +} + +internal class IntValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = Int::class.java + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE) + + override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int +} + +internal class LongValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = Long::class.java + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE) + + override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long +} + +internal class StringValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = String::class.java + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE) + + override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String? +} + +internal class JavaUuidValueClassUnboxConverter( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxedType: Type get() = UUID::class.java + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE) + + override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID? +} + +internal class GenericValueClassUnboxConverter( + override val valueClass: Class, + override val unboxedType: Type, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE) + + override fun convert(value: T): Any? = unboxHandle.invokeExact(value) } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt index 485ae263b..5d49ef118 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt @@ -2,7 +2,11 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.databind.JsonMappingException +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.AnnotatedElement +import java.lang.reflect.Method import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KType @@ -46,3 +50,16 @@ internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean = getAnnotation(Js // Determine if the unbox result of value class is nullable internal fun KClass<*>.wrapsNullable(): Boolean = this.memberProperties.first { it.javaField != null }.returnType.isMarkedNullable + +internal val ANY_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Any::class.java) } +internal val ANY_TO_INT_METHOD_TYPE by lazy {MethodType.methodType(Int::class.java, Any::class.java) } +internal val ANY_TO_LONG_METHOD_TYPE by lazy {MethodType.methodType(Long::class.java, Any::class.java) } +internal val ANY_TO_STRING_METHOD_TYPE by lazy {MethodType.methodType(String::class.java, Any::class.java) } +internal val ANY_TO_JAVA_UUID_METHOD_TYPE by lazy {MethodType.methodType(UUID::class.java, Any::class.java) } +internal val INT_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Int::class.java) } +internal val LONG_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Long::class.java) } +internal val STRING_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, String::class.java) } +internal val JAVA_UUID_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, UUID::class.java) } + +internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method) +internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt index 8658747a8..3bdaa833b 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt @@ -96,7 +96,7 @@ internal class ValueClassKeyDeserializer( @Suppress("UNCHECKED_CAST") converter.convert(creator.invoke(null, value) as S) } catch (e: InvalidDefinitionException) { - throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass.java)), e) + throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e) } } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt index 3960deb5e..b5666f04d 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt @@ -122,7 +122,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: KClass<*>): ValueClassBoxConverter<*, *> = valueClassBoxConverterCache.get(boxedClass) ?: run { - val value = ValueClassBoxConverter(unboxedClass, boxedClass) + val value = ValueClassBoxConverter.create(unboxedClass, boxedClass.java) (valueClassBoxConverterCache.putIfAbsent(boxedClass, value) ?: value) } From 82e6870301971cf4644b1ab9bd2e02cda9383f02 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 21:46:56 +0900 Subject: [PATCH 02/12] Ported deserializers that uses MethodHandles from https://github.com/ProjectMapK/jackson-module-kogera/blob/1579484a6e57303cca065b6897a2ae88ac21ec0c/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt --- .../module/kotlin/KotlinDeserializers.kt | 145 +++++++++++++++++- 1 file changed, 137 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt index 09dd7e022..3a8bc789e 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt @@ -11,9 +11,11 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.Deserializers import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException -import com.fasterxml.jackson.databind.util.ClassUtil +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.UUID import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.javaMethod import kotlin.time.Duration as KotlinDuration @@ -100,14 +102,128 @@ object ULongDeserializer : StdDeserializer(ULong::class.java) { ) } -internal class WrapsNullableValueClassBoxDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter +// If the creator does not perform type conversion, implement a unique deserializer for each for fast invocation. +internal sealed class NoConversionCreatorBoxDeserializer( + creator: Method, + converter: ValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] + protected abstract val inputType: Class<*> + protected val handle: MethodHandle = MethodHandles + .filterReturnValue(unreflect(creator), converter.boxHandle) + + // Since the input to handle must be strict, invoke should be implemented in each class + protected abstract fun invokeExact(value: S): D + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { + // For the sake of commonality, it is unavoidably called without checking. + // It is controlled by KotlinValueInstantiator, so it is not expected to reach this branch. + @Suppress("UNCHECKED_CAST") + invokeExact(null as S) + } + + final override fun getBoxedNullValue(): D = boxedNullValue + + final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + @Suppress("UNCHECKED_CAST") + return invokeExact(p.readValueAs(inputType) as S) + } + + internal class WrapsInt( + creator: Method, + converter: IntValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = Int::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D + } + + internal class WrapsLong( + creator: Method, + converter: LongValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = Long::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D + } + + internal class WrapsString( + creator: Method, + converter: StringValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = String::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } + + internal class WrapsJavaUuid( + creator: Method, + converter: JavaUuidValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = UUID::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + companion object { + fun create(creator: Method, converter: ValueClassBoxConverter.Specified) = when (converter) { + is IntValueClassBoxConverter -> WrapsInt(creator, converter) + is LongValueClassBoxConverter -> WrapsLong(creator, converter) + is StringValueClassBoxConverter -> WrapsString(creator, converter) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(creator, converter) + } + } +} + +// Even if the creator performs type conversion, it is distinguished +// because a speedup due to rtype matching of filterReturnValue can be expected for the specified type. +internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer( + creator: Method, + private val inputType: Class<*>, + converter: ValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val handle: MethodHandle init { - ClassUtil.checkAndFixAccess(creator, false) + val unreflect = unreflect(creator).run { + asType(type().changeParameterType(0, Any::class.java)) + } + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { instantiate(null) } + + override fun getBoxedNullValue(): D = boxedNullValue + + // To instantiate the value class in the same way as other classes, + // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. + // Input is null only when called from KotlinValueInstantiator. + @Suppress("UNCHECKED_CAST") + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + return instantiate(input) + } +} + +internal class WrapsAnyValueClassBoxDeserializer( + creator: Method, + private val inputType: Class<*>, + converter: GenericValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val handle: MethodHandle + + init { + val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } // Cache the result of wrapping null, since the result is always expected to be the same. @@ -120,7 +236,7 @@ internal class WrapsNullableValueClassBoxDeserializer( // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. // Input is null only when called from KotlinValueInstantiator. @Suppress("UNCHECKED_CAST") - private fun instantiate(input: Any?): D = converter.convert(creator.invoke(null, input) as S) + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { val input = p.readValueAs(inputType) @@ -173,7 +289,20 @@ internal class KotlinDeserializers( rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass)?.let { val unboxedClass = it.returnType val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass.kotlin) - WrapsNullableValueClassBoxDeserializer(it, converter) + + when (converter) { + is ValueClassBoxConverter.Specified -> { + val inputType = it.parameterTypes[0] + + if (inputType == unboxedClass) { + NoConversionCreatorBoxDeserializer.create(it, converter) + } else { + HasConversionCreatorWrapsSpecifiedBoxDeserializer(it, inputType, converter) + } + } + is GenericValueClassBoxConverter -> + WrapsAnyValueClassBoxDeserializer(it, it.parameterTypes[0], converter) + } } else -> null } From c01236ff09ad2994a20f95df1dedde21e2d9b4bb Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 21:50:01 +0900 Subject: [PATCH 03/12] Fixing tests for destructively changed behavior Errors thrown by the constructor of value classes are no longer wrapped in InvocationTargetException. --- .../deser/valueClass/WithoutCustomDeserializeMethodTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt index 01db60095..07dde3a64 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException import kotlin.test.assertNotEquals class WithoutCustomDeserializeMethodTest { @@ -132,8 +131,8 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { defaultMapper.readValue("-1") } - assertTrue(e.cause === throwable) + val e = assertThrows { defaultMapper.readValue("-1") } + assertTrue(e === throwable) } // If all JsonCreator tests are OK, no need to check throws from factory functions. From 88e1a51da62a92366f467a20dfe837efbd6763d4 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 21:54:23 +0900 Subject: [PATCH 04/12] Ported keyDeserializers that uses MethodHandles https://github.com/ProjectMapK/jackson-module-kogera/blob/1579484a6e57303cca065b6897a2ae88ac21ec0c/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt --- .../module/kotlin/KotlinKeyDeserializers.kt | 102 +++++++++++++++--- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt index 3bdaa833b..0879f4232 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt @@ -6,8 +6,10 @@ import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializers import com.fasterxml.jackson.databind.exc.InvalidDefinitionException -import com.fasterxml.jackson.databind.util.ClassUtil +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method +import java.util.UUID import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.javaMethod @@ -72,34 +74,100 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(TYPE_LONG, ULong::clas } // The implementation is designed to be compatible with various creators, just in case. -internal class ValueClassKeyDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter +internal sealed class ValueClassKeyDeserializer( + converter: ValueClassBoxConverter, + creatorHandle: MethodHandle, ) : KeyDeserializer() { - private val unboxedClass: Class<*> = creator.parameterTypes[0] + private val boxedClass: Class = converter.boxedClass - init { - ClassUtil.checkAndFixAccess(creator, false) - } + protected abstract val unboxedClass: Class<*> + protected val handle: MethodHandle = MethodHandles.filterReturnValue(creatorHandle, converter.boxHandle) // Based on databind error // https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624 - private fun errorMessage(boxedType: JavaType): String = - "Could not find (Map) Key deserializer for types wrapped in $boxedType" + private fun errorMessage(boxedType: JavaType): String = "Could not find (Map) Key deserializer for types " + + "wrapped in $boxedType" + + // Since the input to handle must be strict, invoke should be implemented in each class + protected abstract fun invokeExact(value: S): D - override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { + final override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { val unboxedJavaType = ctxt.constructType(unboxedClass) return try { // findKeyDeserializer does not return null, and an exception will be thrown if not found. val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt) @Suppress("UNCHECKED_CAST") - converter.convert(creator.invoke(null, value) as S) + invokeExact(value as S) } catch (e: InvalidDefinitionException) { - throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e) + throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(boxedClass)), e) } } + internal sealed class WrapsSpecified( + converter: ValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + // Currently, only the primary constructor can be the creator of a key, so for specified types, + // the return type of the primary constructor and the input type of the box function are exactly the same. + // Therefore, performance is improved by omitting the asType call. + unreflect(creator), + ) + + internal class WrapsInt( + converter: IntValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = Int::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D + } + + internal class WrapsLong( + converter: LongValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = Long::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D + } + + internal class WrapsString( + converter: StringValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = String::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = UUID::class.java + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + internal class WrapsAny( + converter: GenericValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE), + ) { + override val unboxedClass: Class<*> = creator.returnType + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: S): D = handle.invokeExact(value) as D + } + companion object { fun createOrNull( boxedClass: KClass<*>, @@ -113,7 +181,13 @@ internal class ValueClassKeyDeserializer( val creator = boxedClass.primaryConstructor?.javaMethod ?: return null val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass) - return ValueClassKeyDeserializer(creator, converter) + return when (converter) { + is IntValueClassBoxConverter -> WrapsInt(converter, creator) + is LongValueClassBoxConverter -> WrapsLong(converter, creator) + is StringValueClassBoxConverter -> WrapsString(converter, creator) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(converter, creator) + is GenericValueClassBoxConverter -> WrapsAny(converter, creator) + } } } } From bae19d53d67c1753f31b49dde58af6c56485a489 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 21:55:31 +0900 Subject: [PATCH 05/12] Fixing tests for destructively changed behavior Errors thrown by the constructor of value classes are no longer wrapped in InvocationTargetException. --- .../valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt index bf60a014e..250296938 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer class WithoutCustomDeserializeMethodTest { @@ -98,10 +97,10 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { + val e = assertThrows { defaultMapper.readValue>("""{"-1":null}""") } - assertTrue(e.cause === throwable) + assertTrue(e === throwable) } data class Wrapped(val first: String, val second: String) { From f7896b1c068e9307b0d2f1f4534dfafde9a19f55 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 22:10:41 +0900 Subject: [PATCH 06/12] Add cache for ValueClassUnboxConverter --- .../fasterxml/jackson/module/kotlin/ReflectionCache.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt index b5666f04d..cb36e70c0 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt @@ -23,7 +23,7 @@ import kotlin.reflect.jvm.kotlinFunction internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { companion object { // Increment is required when properties that use LRUMap are changed. - private const val serialVersionUID = 4L + private const val serialVersionUID = 5L } private val javaExecutableToKotlin = LRUMap>(reflectionCacheSize, reflectionCacheSize) @@ -38,6 +38,8 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { // since the cache is used only twice locally at initialization per property. private val valueClassBoxConverterCache: LRUMap, ValueClassBoxConverter<*, *>> = LRUMap(0, reflectionCacheSize) + private val valueClassUnboxConverterCache: LRUMap, ValueClassUnboxConverter<*, *>> = + LRUMap(0, reflectionCacheSize) // If the Record type defined in Java is processed, // an error will occur, so if it is not defined in Kotlin, skip the process. @@ -126,6 +128,12 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { (valueClassBoxConverterCache.putIfAbsent(boxedClass, value) ?: value) } + fun getValueClassUnboxConverter(boxedClass: KClass<*>): ValueClassUnboxConverter<*, *> = + valueClassUnboxConverterCache.get(boxedClass) ?: run { + val value = ValueClassUnboxConverter.create(boxedClass.java) + (valueClassUnboxConverterCache.putIfAbsent(boxedClass, value) ?: value) + } + fun findKotlinParameter(param: AnnotatedParameter): KParameter? = when (val owner = param.owner.member) { is Constructor<*> -> kotlinFromJava(owner) is Method -> kotlinFromJava(owner) From f8329b5d3dd78a0c5a03f1155c03042458a81733 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 22:12:51 +0900 Subject: [PATCH 07/12] Ported serializers that uses MethodHandles from https://github.com/ProjectMapK/jackson-module-kogera/blob/4ccec130c72dcbaadc49e405e933c10a984f5dfc/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt --- .../jackson/module/kotlin/KotlinModule.kt | 2 +- .../module/kotlin/KotlinSerializers.kt | 92 ++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index 397d93a4f..0f6db7a13 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -133,7 +133,7 @@ class KotlinModule private constructor( context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion)) context.addKeyDeserializers(KotlinKeyDeserializers(cache)) - context.addSerializers(KotlinSerializers()) + context.addSerializers(KotlinSerializers(cache)) context.addKeySerializers(KotlinKeySerializers()) // ranges diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt index 82bcb164d..3af4c28ef 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.Serializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier import java.math.BigInteger @@ -51,41 +53,80 @@ private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods. Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value } } -object ValueClassUnboxSerializer : StdSerializer(Any::class.java) { - private fun readResolve(): Any = ValueClassUnboxSerializer +internal sealed class ValueClassStaticJsonValueSerializer( + converter: ValueClassUnboxConverter, + staticJsonValueHandle: MethodHandle, +) : StdSerializer(converter.valueClass) { + private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle) - override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = value::class.java.getMethod("unbox-impl").invoke(value) - provider.defaultSerializeValue(unboxed, gen) + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonValue: Any? = handle.invokeExact(value) + provider.defaultSerializeValue(jsonValue, gen) } -} -internal sealed class ValueClassSerializer(t: Class) : StdSerializer(t) { - class StaticJsonValue( - t: Class, private val staticJsonValueGetter: Method - ) : ValueClassSerializer(t) { - private val unboxMethod: Method = t.getMethod("unbox-impl") - - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = unboxMethod.invoke(value) - // As shown in the processing of the factory function, jsonValueGetter is always a static method. - val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed) - provider.defaultSerializeValue(jsonValue, gen) - } - } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE), + ) companion object { // `t` must be UnboxableValueClass. // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun from(t: Class<*>): StdSerializer<*> = t.getStaticJsonValueGetter() - ?.let { StaticJsonValue(t, it) } - ?: ValueClassUnboxSerializer + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonValueSerializer? = converter + .valueClass + .getStaticJsonValueGetter() + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } } -internal class KotlinSerializers : Serializers.Base() { +internal class KotlinSerializers(private val cache: ReflectionCache) : Serializers.Base() { override fun findSerializer( config: SerializationConfig?, type: JavaType, @@ -99,7 +140,10 @@ internal class KotlinSerializers : Serializers.Base() { UInt::class.java == rawClass -> UIntSerializer ULong::class.java == rawClass -> ULongSerializer // The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers. - rawClass.isUnboxableValueClass() -> ValueClassSerializer.from(rawClass) + rawClass.isUnboxableValueClass() -> { + val unboxConverter = cache.getValueClassUnboxConverter(rawClass.kotlin) + ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer + } else -> null } } From 716e440c3ceae28b30a8d8c135e8ec50fa86b359 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 22:16:41 +0900 Subject: [PATCH 08/12] Ported keySerializers that uses MethodHandles from https://github.com/ProjectMapK/jackson-module-kogera/blob/81a5c8feced5a316137105e4057eb8a9de47036e/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt --- .../module/kotlin/KotlinKeySerializers.kt | 131 ++++++++++++++---- .../jackson/module/kotlin/KotlinModule.kt | 2 +- 2 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt index c6a5d7747..d3a4bb219 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt @@ -9,18 +9,20 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.Serializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier -internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.java) { - private fun readResolve(): Any = ValueClassUnboxKeySerializer - - override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { - val method = value::class.java.getMethod("unbox-impl") - val unboxed = method.invoke(value) +internal class ValueClassUnboxKeySerializer( + private val converter: ValueClassUnboxConverter, +) : StdSerializer(converter.valueClass) { + override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val unboxed = converter.convert(value) if (unboxed == null) { - val javaType = provider.typeFactory.constructType(method.genericReturnType) + val javaType = converter.getOutputType(provider.typeFactory) provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider) return } @@ -29,21 +31,18 @@ internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.jav } } -// Class must be UnboxableValueClass. -private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> - Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } -} - -internal class ValueClassStaticJsonKeySerializer( - t: Class, - private val staticJsonKeyGetter: Method -) : StdSerializer(t) { - private val keyType: Class<*> = staticJsonKeyGetter.returnType - private val unboxMethod: Method = t.getMethod("unbox-impl") +internal sealed class ValueClassStaticJsonKeySerializer( + converter: ValueClassUnboxConverter, + staticJsonValueGetter: Method, + methodType: MethodType, +) : StdSerializer(converter.valueClass) { + private val keyType: Class<*> = staticJsonValueGetter.returnType + private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let { + MethodHandles.filterReturnValue(converter.unboxHandle, it) + } - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = unboxMethod.invoke(value) - val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed) + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonKey: Any? = handle.invokeExact(value) val serializer = jsonKey ?.let { provider.findKeySerializer(keyType, null) } @@ -52,20 +51,94 @@ internal class ValueClassStaticJsonKeySerializer( serializer.serialize(jsonKey, gen, provider) } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + INT_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + LONG_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + STRING_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + JAVA_UUID_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + ANY_TO_ANY_METHOD_TYPE, + + ) + companion object { - fun createOrNull(t: Class<*>): StdSerializer<*>? = - t.getStaticJsonKeyGetter()?.let { ValueClassStaticJsonKeySerializer(t, it) } + // Class must be UnboxableValueClass. + private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> + Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } + } + + // `t` must be UnboxableValueClass. + // If create a function with a JsonValue in the value class, + // it will be compiled as a static method (= cannot be processed properly by Jackson), + // so use a ValueClassSerializer.StaticJsonValue to handle this. + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonKeySerializer? = converter + .valueClass + .getStaticJsonKeyGetter() + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } } -internal class KotlinKeySerializers : Serializers.Base() { +internal class KotlinKeySerializers(private val cache: ReflectionCache) : Serializers.Base() { override fun findSerializer( config: SerializationConfig, type: JavaType, - beanDesc: BeanDescription - ): JsonSerializer<*>? = when { - type.rawClass.isUnboxableValueClass() -> ValueClassStaticJsonKeySerializer.createOrNull(type.rawClass) - ?: ValueClassUnboxKeySerializer - else -> null + beanDesc: BeanDescription, + ): JsonSerializer<*>? { + val rawClass = type.rawClass + + return when { + rawClass.isUnboxableValueClass() -> { + val unboxConverter = cache.getValueClassUnboxConverter(rawClass.kotlin) + ValueClassStaticJsonKeySerializer.createOrNull(unboxConverter) + ?: ValueClassUnboxKeySerializer(unboxConverter) + } + else -> null + } } } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index 0f6db7a13..d23afd4ef 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -134,7 +134,7 @@ class KotlinModule private constructor( context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion)) context.addKeyDeserializers(KotlinKeyDeserializers(cache)) context.addSerializers(KotlinSerializers(cache)) - context.addKeySerializers(KotlinKeySerializers()) + context.addKeySerializers(KotlinKeySerializers(cache)) // ranges context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java) From 05742800600b2f845ec37ceca81e3d0cedb24405 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 22:19:47 +0900 Subject: [PATCH 09/12] Fixed cache to minimize the number of conversions --- .../jackson/module/kotlin/KotlinDeserializers.kt | 2 +- .../module/kotlin/KotlinKeySerializers.kt | 2 +- .../jackson/module/kotlin/KotlinSerializers.kt | 2 +- .../jackson/module/kotlin/ReflectionCache.kt | 16 ++++++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt index 3a8bc789e..9ece03809 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt @@ -288,7 +288,7 @@ internal class KotlinDeserializers( JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass)?.let { val unboxedClass = it.returnType - val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass.kotlin) + val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass) when (converter) { is ValueClassBoxConverter.Specified -> { diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt index d3a4bb219..d6e0b10e6 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt @@ -134,7 +134,7 @@ internal class KotlinKeySerializers(private val cache: ReflectionCache) : Serial return when { rawClass.isUnboxableValueClass() -> { - val unboxConverter = cache.getValueClassUnboxConverter(rawClass.kotlin) + val unboxConverter = cache.getValueClassUnboxConverter(rawClass) ValueClassStaticJsonKeySerializer.createOrNull(unboxConverter) ?: ValueClassUnboxKeySerializer(unboxConverter) } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt index 3af4c28ef..9d381b41a 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -141,7 +141,7 @@ internal class KotlinSerializers(private val cache: ReflectionCache) : Serialize ULong::class.java == rawClass -> ULongSerializer // The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers. rawClass.isUnboxableValueClass() -> { - val unboxConverter = cache.getValueClassUnboxConverter(rawClass.kotlin) + val unboxConverter = cache.getValueClassUnboxConverter(rawClass) ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer } else -> null diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt index cb36e70c0..f7762dfce 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt @@ -12,6 +12,7 @@ import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Method import java.util.* +import kotlin.jvm.java import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter @@ -36,9 +37,9 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { // TODO: Consider whether the cache size should be reduced more, // since the cache is used only twice locally at initialization per property. - private val valueClassBoxConverterCache: LRUMap, ValueClassBoxConverter<*, *>> = + private val valueClassBoxConverterCache: LRUMap, ValueClassBoxConverter<*, *>> = LRUMap(0, reflectionCacheSize) - private val valueClassUnboxConverterCache: LRUMap, ValueClassUnboxConverter<*, *>> = + private val valueClassUnboxConverterCache: LRUMap, ValueClassUnboxConverter<*, *>> = LRUMap(0, reflectionCacheSize) // If the Record type defined in Java is processed, @@ -122,15 +123,18 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable { }.orElse(null) } - fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: KClass<*>): ValueClassBoxConverter<*, *> = + fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: Class<*>): ValueClassBoxConverter<*, *> = valueClassBoxConverterCache.get(boxedClass) ?: run { - val value = ValueClassBoxConverter.create(unboxedClass, boxedClass.java) + val value = ValueClassBoxConverter.create(unboxedClass, boxedClass) (valueClassBoxConverterCache.putIfAbsent(boxedClass, value) ?: value) } - fun getValueClassUnboxConverter(boxedClass: KClass<*>): ValueClassUnboxConverter<*, *> = + fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: KClass<*>): ValueClassBoxConverter<*, *> = + getValueClassBoxConverter(unboxedClass, boxedClass.java) + + fun getValueClassUnboxConverter(boxedClass: Class<*>): ValueClassUnboxConverter<*, *> = valueClassUnboxConverterCache.get(boxedClass) ?: run { - val value = ValueClassUnboxConverter.create(boxedClass.java) + val value = ValueClassUnboxConverter.create(boxedClass) (valueClassUnboxConverterCache.putIfAbsent(boxedClass, value) ?: value) } From fe1e1cb684e5407906e4dee1d703de1049e42196 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 22:48:49 +0900 Subject: [PATCH 10/12] Revert public class that were accidentally deleted with deprecation --- .../jackson/module/kotlin/KotlinSerializers.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt index 9d381b41a..42aabc2fa 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -48,6 +48,19 @@ object ULongSerializer : StdSerializer(ULong::class.java) { } } +@Deprecated( + message = "This class was published by mistake. It will be removed in `2.22.0` as it is no longer used internally.", + level = DeprecationLevel.WARNING +) +object ValueClassUnboxSerializer : StdSerializer(Any::class.java) { + private fun readResolve(): Any = ValueClassUnboxSerializer + + override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { + val unboxed = value::class.java.getMethod("unbox-impl").invoke(value) + provider.defaultSerializeValue(unboxed, gen) + } +} + // Class must be UnboxableValueClass. private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method -> Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value } From d81fb8282b6a8337c2cf7601bc736f1e0c463157 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sat, 5 Jul 2025 23:36:41 +0900 Subject: [PATCH 11/12] Modify japicmp excludes --- pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pom.xml b/pom.xml index 384c965d2..a822d5e69 100644 --- a/pom.xml +++ b/pom.xml @@ -255,6 +255,22 @@ com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException#MissingKotlinParameterException(kotlin.reflect.KParameter,java.io.Closeable,java.lang.String) + + com.fasterxml.jackson.module.kotlin.WrapsNullableValueClassBoxDeserializer + + com.fasterxml.jackson.module.kotlin.ValueClassUnboxKeySerializer + com.fasterxml.jackson.module.kotlin.KotlinKeySerializersKt + com.fasterxml.jackson.module.kotlin.ValueClassSerializer + + + com.fasterxml.jackson.module.kotlin.KotlinKeySerializers#KotlinKeySerializers() + + + com.fasterxml.jackson.module.kotlin.KotlinSerializers#KotlinSerializers() + + com.fasterxml.jackson.module.kotlin.ValueClassStaticJsonKeySerializer + com.fasterxml.jackson.module.kotlin.ValueClassBoxConverter + com.fasterxml.jackson.module.kotlin.ValueClassKeyDeserializer From d6f64c8646caee8ee9024a38b47c08b162e7df6a Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 6 Jul 2025 01:57:08 +0900 Subject: [PATCH 12/12] Update release notes wrt #1018 --- release-notes/CREDITS-2.x | 1 + release-notes/VERSION-2.x | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 8e07fc18f..0572b9a44 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -18,6 +18,7 @@ Contributors: # 2.20.0 (not yet released) WrongWrong (@k163377) +* #1018: Use MethodHandle in processing related to value class * #969: Cleanup of deprecated contents * #967: Update settings for 2.20 diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 522ac1bff..d673f62a5 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -17,7 +17,12 @@ Co-maintainers: ------------------------------------------------------------------------ 2.20.0 (not yet released) - +#1018: Improved handling of `value class` has improved performance for both serialization and deserialization. + In particular, for serialization, proper caching has improved throughput by a factor of 2 or more in the general cases. + Also, replacing function execution by reflection with `MethodHandle` improved throughput by several percent for both serialization and deserialization. + In cases where the number of properties of a `value class` in the processing target is large, there is a possibility to obtain a larger improvement. + Please note that this modification causes a destructive change in that exceptions thrown during deserialization of + `value class` are no longer wrapped in an `InvocationTargetException`. #969: Deprecated content has been cleaned up with the version upgrade. #967: Kotlin has been upgraded to 2.0.21. - Generate SBOMs [JSTEP-14]