Skip to content

Commit

Permalink
Merge pull request #121 from ProjectMapK/develop
Browse files Browse the repository at this point in the history
For release 2.15.2-beta1
  • Loading branch information
k163377 authored Jul 31, 2023
2 parents 67e0edb + 81c6497 commit 5637993
Show file tree
Hide file tree
Showing 19 changed files with 239 additions and 185 deletions.
16 changes: 9 additions & 7 deletions docs/AboutValueClassSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ The `jackson-module-kogera` supports many use cases of `value class` (`inline cl
This page summarizes the basic policy and points to note regarding the use of the `value class`.

## Note on the use of `value class`
`value class` is one of the distinctive features of `Kotlin`.
Many key use cases are not supported in `jackson-module-kotlin` because
the functions and properties associated with `value class` have a special representation on the `JVM`.
The `value class` is one of the `Kotlin` specific feature.
On the other hand, `jackson-module-kotlin` does not support deserialization of `value class` in particular.
Also, there are some features of serialization that do not work properly.

However, due to `Jackson` limitations, the same behavior as the normal class is not fully reproduced.
The reason for this is that `value class` is a special representation on the `JVM`.
Due to this difference, some cases cannot be handled by basic `Jackson` parsing, which assumes `Java`.
Known issues related to `value class` can be found [here](https://github.com/ProjectMapK/jackson-module-kogera/issues?q=is%3Aissue+is%3Aopen+label%3A%22value+class%22).

Also, when using `Jackson`, there is a concern that the use of `value class` will rather degrade performance.
This is because `jackson-module-kogera` does a lot of reflection processing to support `value class`
(this concern will be confirmed in [kogera-benchmark](https://github.com/ProjectMapK/kogera-benchmark) in the future).
In addition, one of the features of the `value class` is improved performance,
but when using `Jackson` (not only `Jackson`, but also other libraries that use reflection),
the performance is rather reduced.
This can be confirmed from [kogera-benchmark](https://github.com/ProjectMapK/kogera-benchmark).

For these reasons, I recommend careful consideration when using `value class`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,18 @@ package io.github.projectmapk.jackson.module.kogera

import com.fasterxml.jackson.annotation.JsonCreator
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmClassifier
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmProperty
import kotlinx.metadata.KmType
import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.jvm.JvmFieldSignature
import kotlinx.metadata.jvm.JvmMethodSignature
import kotlinx.metadata.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.getterSignature
import kotlinx.metadata.jvm.signature
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method

internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline }

internal fun Class<*>.toKmClass(): KmClass? = annotations
.filterIsInstance<Metadata>()
.firstOrNull()
?.let { KotlinClassMetadata.read(it) as KotlinClassMetadata.Class }
?.toKmClass()

private val primitiveClassToDesc by lazy {
mapOf(
Byte::class.javaPrimitiveType to 'B',
Expand Down Expand Up @@ -106,31 +94,6 @@ internal fun String.reconstructClass(): Class<*> {
internal fun KmType.reconstructClassOrNull(): Class<*>? = (classifier as? KmClassifier.Class)
?.let { kotlin.runCatching { it.name.reconstructClass() }.getOrNull() }

internal fun KmClass.findKmConstructor(constructor: Constructor<*>): KmConstructor? {
val descHead = constructor.parameterTypes.toDescBuilder()
val desc = CharArray(descHead.length + 1).apply {
descHead.getChars(0, descHead.length, this, 0)
this[this.lastIndex] = 'V'
}.let { String(it) }

// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
val valueDesc = descHead
.deleteCharAt(descHead.length - 1)
.append("Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
.toString()

// Constructors always have the same name, so only desc is compared
return constructors.find {
val targetDesc = it.signature?.desc
targetDesc == desc || targetDesc == valueDesc
}
}

internal fun KmClass.findPropertyByGetter(getter: Method): KmProperty? {
val signature = getter.toSignature()
return properties.find { it.getterSignature == signature }
}

internal fun KmType.isNullable(): Boolean = Flag.Type.IS_NULLABLE(this.flags)

internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.github.projectmapk.jackson.module.kogera

import kotlinx.metadata.ClassName
import kotlinx.metadata.Flags
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmProperty
import kotlinx.metadata.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.getterSignature
import kotlinx.metadata.jvm.signature
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method

private fun Class<*>.toKmClass(): KmClass? = annotations
.filterIsInstance<Metadata>()
.firstOrNull()
?.let { KotlinClassMetadata.read(it) as KotlinClassMetadata.Class }
?.toKmClass()

// Jackson Metadata Class
internal class JmClass(
private val clazz: Class<*>,
kmClass: KmClass
) {
val flags: Flags = kmClass.flags
val constructors: List<KmConstructor> = kmClass.constructors
val properties: List<KmProperty> = kmClass.properties
private val functions: List<KmFunction> = kmClass.functions
val sealedSubclasses: List<ClassName> = kmClass.sealedSubclasses
private val companionPropName: String? = kmClass.companionObject
val companion: CompanionObject? by lazy { companionPropName?.let { CompanionObject(clazz, it) } }

fun findKmConstructor(constructor: Constructor<*>): KmConstructor? {
val descHead = constructor.parameterTypes.toDescBuilder()
val desc = CharArray(descHead.length + 1).apply {
descHead.getChars(0, descHead.length, this, 0)
this[this.lastIndex] = 'V'
}.let { String(it) }

// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
val valueDesc = descHead
.deleteCharAt(descHead.length - 1)
.append("Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
.toString()

// Constructors always have the same name, so only desc is compared
return constructors.find {
val targetDesc = it.signature?.desc
targetDesc == desc || targetDesc == valueDesc
}
}

fun findPropertyByGetter(getter: Method): KmProperty? {
val signature = getter.toSignature()
return properties.find { it.getterSignature == signature }
}

fun findFunctionByMethod(method: Method): KmFunction? {
val signature = method.toSignature()
return functions.find { it.signature == signature }
}

internal class CompanionObject(
declaringClass: Class<*>,
companionObject: String
) {
private val companionField: Field = declaringClass.getDeclaredField(companionObject)
val type: Class<*> = companionField.type
val isAccessible: Boolean = companionField.isAccessible
private val kmClass: KmClass by lazy { type.toKmClass()!! }
val instance: Any by lazy {
// To prevent the call from failing, save the initial value and then rewrite the flag.
if (!companionField.isAccessible) companionField.isAccessible = true
companionField.get(null)
}

fun findFunctionByMethod(method: Method): KmFunction? {
val signature = method.toSignature()
return kmClass.functions.find { it.signature == signature }
}
}

companion object {
fun createOrNull(clazz: Class<*>): JmClass? = clazz.toKmClass()?.let { JmClass(clazz, it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,25 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
* may contain null values after deserialization.
* Enabling it protects against this but has performance impact.
*/
StrictNullChecks(enabledByDefault = false);
StrictNullChecks(enabledByDefault = false),

/**
* This feature represents whether to include in Jackson's parsing the annotations given to the parameters of
* constructors that include value class as a parameter.
*
* Constructor with value class as a parameter on Kotlin are compiled into a public synthetic constructor
* and a private constructor.
* In this case, annotations are only given to synthetic constructors,
* so they are not normally included in Jackson's parsing and will not work.
*
* To work around this problem, annotations can be added by specifying a field or getter,
* or by enabling this feature.
* However, enabling this feature will affect initialization performance.
* Also note that enabling this feature does not enable annotations given to the constructor.
*
* @see KotlinClassIntrospector
*/
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false);

internal val bitSet: BitSet = (1 shl ordinal).toBitSet()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.projectmapk.jackson.module.kogera

import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.CopySyntheticConstructorParameterAnnotations
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullIsSameAsDefault
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyCollection
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyMap
Expand Down Expand Up @@ -40,15 +41,18 @@ public class KotlinModule private constructor(
public val nullToEmptyMap: Boolean = NullToEmptyMap.enabledByDefault,
public val nullIsSameAsDefault: Boolean = NullIsSameAsDefault.enabledByDefault,
public val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
public val copySyntheticConstructorParameterAnnotations: Boolean =
CopySyntheticConstructorParameterAnnotations.enabledByDefault
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
private constructor(builder: Builder) : this(
builder.reflectionCacheSize,
builder.isEnabled(NullToEmptyCollection),
builder.isEnabled(NullToEmptyMap),
builder.isEnabled(NullIsSameAsDefault),
builder.isEnabled(SingletonSupport),
builder.isEnabled(StrictNullChecks)
builder.isEnabled(StrictNullChecks),
builder.isEnabled(CopySyntheticConstructorParameterAnnotations)
)

@Deprecated(
Expand Down Expand Up @@ -77,15 +81,17 @@ public class KotlinModule private constructor(
)

if (singletonSupport) {
context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier)
context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier(cache))
}

context.insertAnnotationIntrospector(
KotlinPrimaryAnnotationIntrospector(nullToEmptyCollection, nullToEmptyMap, cache)
)
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(this, strictNullChecks, cache))
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(strictNullChecks, cache))

context.setClassIntrospector(KotlinClassIntrospector)
if (copySyntheticConstructorParameterAnnotations) {
context.setClassIntrospector(KotlinClassIntrospector)
}

context.addDeserializers(KotlinDeserializers(cache))
context.addKeyDeserializers(KotlinKeyDeserializers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.crea
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.MethodValueCreator
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ValueCreator
import io.github.projectmapk.jackson.module.kogera.ser.ValueClassBoxConverter
import kotlinx.metadata.KmClass
import java.io.Serializable
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
Expand All @@ -16,11 +15,12 @@ import java.util.Optional
internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
companion object {
// Increment is required when properties that use LRUMap are changed.
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L
}

// This cache is used for both serialization and deserialization, so reserve a larger size from the start.
private val classCache = LRUMap<Class<*>, Optional<KmClass>>(reflectionCacheSize, reflectionCacheSize)
private val classCache = LRUMap<Class<*>, Optional<JmClass>>(reflectionCacheSize, reflectionCacheSize)
private val creatorCache: LRUMap<Executable, ValueCreator<*>>

// Initial size is 0 because the value class is not always used
Expand All @@ -47,13 +47,13 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
creatorCache = LRUMap(initialEntries, reflectionCacheSize)
}

fun getKmClass(clazz: Class<*>): KmClass? {
fun getJmClass(clazz: Class<*>): JmClass? {
val optional = classCache.get(clazz)

return if (optional != null) {
optional
} else {
val value = Optional.ofNullable(clazz.toKmClass())
val value = Optional.ofNullable(JmClass.createOrNull(clazz))
(classCache.putIfAbsent(clazz, value) ?: value)
}.orElse(null)
}
Expand All @@ -65,7 +65,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
is Constructor<*> -> {
creatorCache.get(creator)
?: run {
getKmClass(creator.declaringClass)?.let {
getJmClass(creator.declaringClass)?.let {
val value = ConstructorValueCreator(creator, it)
creatorCache.putIfAbsent(creator, value) ?: value
}
Expand All @@ -75,7 +75,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
is Method -> {
creatorCache.get(creator)
?: run {
getKmClass(creator.declaringClass)?.let {
getJmClass(creator.declaringClass)?.let {
val value = MethodValueCreator<Any?>(creator, it)
creatorCache.putIfAbsent(creator, value) ?: value
}
Expand All @@ -94,7 +94,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
// TODO: Verify the case where a value class encompasses another value class.
if (this.returnType.isUnboxableValueClass()) return null
}
val kotlinProperty = getKmClass(getter.declaringClass)?.findPropertyByGetter(getter)
val kotlinProperty = getJmClass(getter.declaringClass)?.findPropertyByGetter(getter)

// Since there was no way to directly determine whether returnType is a value class or not,
// Class is restored and processed.
Expand Down
Loading

0 comments on commit 5637993

Please sign in to comment.