diff --git a/pom.xml b/pom.xml index 384c965d..a822d5e6 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 diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 8e07fc18..0572b9a4 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 522ac1bf..d673f62a 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] 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 c284fb8c..7902b072 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 485ae263..5d49ef11 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/KotlinDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt index 09dd7e02..9ece0380 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) @@ -172,8 +288,21 @@ internal class KotlinDeserializers( JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass)?.let { val unboxedClass = it.returnType - val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass.kotlin) - WrapsNullableValueClassBoxDeserializer(it, converter) + val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass) + + 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 } 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 8658747a..0879f423 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.java)), 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) + } } } } 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 c6a5d774..d6e0b10e 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) + 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 397d93a4..d23afd4e 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -133,8 +133,8 @@ class KotlinModule private constructor( context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion)) context.addKeyDeserializers(KotlinKeyDeserializers(cache)) - context.addSerializers(KotlinSerializers()) - context.addKeySerializers(KotlinKeySerializers()) + context.addSerializers(KotlinSerializers(cache)) + context.addKeySerializers(KotlinKeySerializers(cache)) // ranges context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java) 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 82bcb164..42aabc2f 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 @@ -46,11 +48,10 @@ object ULongSerializer : StdSerializer(ULong::class.java) { } } -// 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 } -} - +@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 @@ -60,32 +61,85 @@ object ValueClassUnboxSerializer : StdSerializer(Any::class.java) { } } -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) - } +// 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 } +} + +internal sealed class ValueClassStaticJsonValueSerializer( + converter: ValueClassUnboxConverter, + staticJsonValueHandle: MethodHandle, +) : StdSerializer(converter.valueClass) { + private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle) + + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonValue: Any? = handle.invokeExact(value) + 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 +153,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) + 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 3960deb5..f7762dfc 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 @@ -23,7 +24,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) @@ -36,7 +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<*, *>> = LRUMap(0, reflectionCacheSize) // If the Record type defined in Java is processed, @@ -120,12 +123,21 @@ 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(unboxedClass, boxedClass) + val value = ValueClassBoxConverter.create(unboxedClass, boxedClass) (valueClassBoxConverterCache.putIfAbsent(boxedClass, value) ?: value) } + 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) + (valueClassUnboxConverterCache.putIfAbsent(boxedClass, value) ?: value) + } + fun findKotlinParameter(param: AnnotatedParameter): KParameter? = when (val owner = param.owner.member) { is Constructor<*> -> kotlinFromJava(owner) is Method -> kotlinFromJava(owner) 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 01db6009..07dde3a6 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. 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 bf60a014..25029693 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) {