Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,22 @@
<exclude>
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException#MissingKotlinParameterException(kotlin.reflect.KParameter,java.io.Closeable,java.lang.String)
</exclude>
<exclude>
com.fasterxml.jackson.module.kotlin.WrapsNullableValueClassBoxDeserializer
</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.ValueClassUnboxKeySerializer</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.KotlinKeySerializersKt</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.ValueClassSerializer</exclude>
<!-- internal -->
<exclude>
com.fasterxml.jackson.module.kotlin.KotlinKeySerializers#KotlinKeySerializers()
</exclude>
<exclude>
com.fasterxml.jackson.module.kotlin.KotlinSerializers#KotlinSerializers()
</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.ValueClassStaticJsonKeySerializer</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.ValueClassBoxConverter</exclude>
<exclude>com.fasterxml.jackson.module.kotlin.ValueClassKeyDeserializer</exclude>
</excludes>
</parameter>
</configuration>
Expand Down
1 change: 1 addition & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
173 changes: 161 additions & 12 deletions src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,7 +27,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
}

internal object KotlinDurationValueToJavaDurationConverter : StdConverter<Long, JavaDuration>() {
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))
}
Expand All @@ -45,18 +49,163 @@ internal object JavaToKotlinDurationConverter : StdConverter<JavaDuration, Kotli
}
}

// S is nullable because value corresponds to a nullable value class
// @see KotlinNamesAnnotationIntrospector.findNullSerializer
internal class ValueClassBoxConverter<S : Any?, D : Any>(
unboxedClass: Class<S>,
val boxedClass: KClass<D>
) : StdConverter<S, D>() {
private val boxMethod = boxedClass.java.getDeclaredMethod("box-impl", unboxedClass).apply {
ClassUtil.checkAndFixAccess(this, false)
internal sealed class ValueClassBoxConverter<S : Any?, D : Any> : StdConverter<S, D>() {
abstract val boxedClass: Class<D>
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<S : Any?, D : Any> : ValueClassBoxConverter<S, D>()
}

// region: Converters for common classes as wrapped values, add as needed.
internal class IntValueClassBoxConverter<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<Int, D>() {
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<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<Long, D>() {
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<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<String?, D>() {
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<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<UUID?, D>() {
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<S : Any?, D : Any>(
unboxedClass: Class<S>,
override val boxedClass: Class<D>,
) : ValueClassBoxConverter<S, D>() {
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<S : Any, D : Any?> : StdConverter<S, D>() {
abstract val valueClass: Class<S>
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Int>() {
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Long>() {
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, String?>() {
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, UUID?>() {
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<T : Any>(
override val valueClass: Class<T>,
override val unboxedType: Type,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Any?>() {
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE)

override fun convert(value: T): Any? = unboxHandle.invokeExact(value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Loading