Skip to content

Commit fdf7601

Browse files
authored
Merge pull request #128 from ProjectMapK/develop
For release 2.15.2-beta2
2 parents 5637993 + 673ae04 commit fdf7601

File tree

12 files changed

+283
-149
lines changed

12 files changed

+283
-149
lines changed

.github/workflows/test-main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
# LTS versions, latest version (if exists)
3737
java-version: [ '8', '11', '17', '19' ]
3838
# Minimum version, latest release version, latest pre-release version (if exists)
39-
kotlin-version: [ '1.7.21', '1.8.22', '1.9.0-Beta' ]
39+
kotlin-version: [ '1.7.21', '1.8.22', '1.9.0' ]
4040
env:
4141
KOTLIN_VERSION: ${{ matrix.kotlin-version }}
4242
name: test-main

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ This project has the following features compared to `jackson-module-kotlin`.
99
- Smaller memory consumption
1010
- More `Kotlin` friendly behavior
1111

12+
Especially when it comes to deserialization throughput, it is several times higher than `jackson-module-kotlin`.
13+
![](https://docs.google.com/spreadsheets/d/e/2PACX-1vTZB9ByuRV9XS_eug0vM_IEx_Em_ObiuZMoClXAt7zVZQZ9EnhKCXmbTsRQpoLiBbje6H_R9Hf7v0RI/pubchart?oid=754117157&format=image)
14+
1215
This project is experimental, but passes all the tests implemented in `jackson-module-kotlin` except for the intentional incompatibility.
1316

1417
# Features of `jackson-module-kogera`
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.github.projectmapk.jackson.module.kogera.deser.value_instantiator;
2+
3+
import kotlin.jvm.functions.Function0;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
import java.lang.ref.SoftReference;
8+
9+
// ported from kotlin.reflect.jvm.internal.ReflectProperties 887dc to use LazySoft
10+
class ReflectProperties {
11+
static abstract class Val<T> {
12+
private static final Object NULL_VALUE = new Object() {
13+
};
14+
15+
@SuppressWarnings({"UnusedParameters", "unused"})
16+
final T getValue(Object instance, Object metadata) {
17+
return invoke();
18+
}
19+
20+
abstract T invoke();
21+
22+
protected Object escape(T value) {
23+
return value == null ? NULL_VALUE : value;
24+
}
25+
26+
@SuppressWarnings("unchecked")
27+
protected T unescape(Object value) {
28+
return value == NULL_VALUE ? null : (T) value;
29+
}
30+
}
31+
32+
// A delegate for a lazy property on a soft reference, whose initializer may be invoked multiple times
33+
// including simultaneously from different threads
34+
static class LazySoftVal<T> extends Val<T> implements Function0<T> {
35+
private final Function0<T> initializer;
36+
private volatile SoftReference<Object> value = null;
37+
38+
LazySoftVal(@Nullable T initialValue, @NotNull Function0<T> initializer) {
39+
this.initializer = initializer;
40+
if (initialValue != null) {
41+
this.value = new SoftReference<Object>(escape(initialValue));
42+
}
43+
}
44+
45+
@Override
46+
public T invoke() {
47+
SoftReference<Object> cached = value;
48+
if (cached != null) {
49+
Object result = cached.get();
50+
if (result != null) {
51+
return unescape(result);
52+
}
53+
}
54+
55+
T result = initializer.invoke();
56+
value = new SoftReference<Object>(escape(result));
57+
58+
return result;
59+
}
60+
}
61+
62+
@NotNull
63+
static <T> LazySoftVal<T> lazySoft(@Nullable T initialValue, @NotNull Function0<T> initializer) {
64+
return new LazySoftVal<T>(initialValue, initializer);
65+
}
66+
67+
@NotNull
68+
static <T> LazySoftVal<T> lazySoft(@NotNull Function0<T> initializer) {
69+
return lazySoft(null, initializer);
70+
}
71+
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ package io.github.projectmapk.jackson.module.kogera
22

33
import com.fasterxml.jackson.annotation.JsonCreator
44
import kotlinx.metadata.Flag
5+
import kotlinx.metadata.KmClass
56
import kotlinx.metadata.KmClassifier
67
import kotlinx.metadata.KmType
78
import kotlinx.metadata.KmValueParameter
89
import kotlinx.metadata.jvm.JvmFieldSignature
910
import kotlinx.metadata.jvm.JvmMethodSignature
11+
import kotlinx.metadata.jvm.KotlinClassMetadata
1012
import java.lang.reflect.AnnotatedElement
1113
import java.lang.reflect.Constructor
1214
import java.lang.reflect.Field
1315
import java.lang.reflect.Method
1416

15-
internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline }
17+
internal fun Class<*>.isUnboxableValueClass() = this.getAnnotation(JvmInline::class.java) != null
18+
19+
internal fun Class<*>.toKmClass(): KmClass? = this.getAnnotation(Metadata::class.java)
20+
?.let { (KotlinClassMetadata.read(it) as KotlinClassMetadata.Class).toKmClass() }
1621

1722
private val primitiveClassToDesc by lazy {
1823
mapOf(
@@ -96,5 +101,6 @@ internal fun KmType.reconstructClassOrNull(): Class<*>? = (classifier as? KmClas
96101

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

99-
internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean =
100-
annotations.any { it is JsonCreator && it.mode != JsonCreator.Mode.DISABLED }
104+
internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean = getAnnotation(JsonCreator::class.java)
105+
?.let { it.mode != JsonCreator.Mode.DISABLED }
106+
?: false

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

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,41 @@ import kotlinx.metadata.KmClass
66
import kotlinx.metadata.KmConstructor
77
import kotlinx.metadata.KmFunction
88
import kotlinx.metadata.KmProperty
9-
import kotlinx.metadata.jvm.KotlinClassMetadata
9+
import kotlinx.metadata.jvm.fieldSignature
1010
import kotlinx.metadata.jvm.getterSignature
1111
import kotlinx.metadata.jvm.signature
1212
import java.lang.reflect.Constructor
1313
import java.lang.reflect.Field
1414
import java.lang.reflect.Method
1515

16-
private fun Class<*>.toKmClass(): KmClass? = annotations
17-
.filterIsInstance<Metadata>()
18-
.firstOrNull()
19-
?.let { KotlinClassMetadata.read(it) as KotlinClassMetadata.Class }
20-
?.toKmClass()
21-
2216
// Jackson Metadata Class
2317
internal class JmClass(
2418
private val clazz: Class<*>,
25-
kmClass: KmClass
19+
kmClass: KmClass,
20+
superJmClass: JmClass?,
21+
interfaceJmClasses: List<JmClass>
2622
) {
23+
private val allPropsMap: Map<String, KmProperty> = mutableMapOf<String, KmProperty>().apply {
24+
kmClass.properties.forEach {
25+
this[it.name] = it
26+
}
27+
28+
// Add properties of inherited classes and interfaces
29+
// If an `interface` is implicitly implemented by an abstract class,
30+
// it is necessary to obtain a more specific type, so always add it from the abstract class first.
31+
superJmClass?.allPropsMap?.forEach {
32+
this.putIfAbsent(it.key, it.value)
33+
}
34+
interfaceJmClasses.forEach { i ->
35+
i.allPropsMap.forEach {
36+
this.putIfAbsent(it.key, it.value)
37+
}
38+
}
39+
}
40+
2741
val flags: Flags = kmClass.flags
2842
val constructors: List<KmConstructor> = kmClass.constructors
29-
val properties: List<KmProperty> = kmClass.properties
30-
private val functions: List<KmFunction> = kmClass.functions
43+
val properties: List<KmProperty> = allPropsMap.values.toList()
3144
val sealedSubclasses: List<ClassName> = kmClass.sealedSubclasses
3245
private val companionPropName: String? = kmClass.companionObject
3346
val companion: CompanionObject? by lazy { companionPropName?.let { CompanionObject(clazz, it) } }
@@ -52,16 +65,15 @@ internal class JmClass(
5265
}
5366
}
5467

68+
// Field name always matches property name
69+
fun findPropertyByField(field: Field): KmProperty? = allPropsMap[field.name]
70+
?.takeIf { it.fieldSignature == field.toSignature() }
71+
5572
fun findPropertyByGetter(getter: Method): KmProperty? {
5673
val signature = getter.toSignature()
5774
return properties.find { it.getterSignature == signature }
5875
}
5976

60-
fun findFunctionByMethod(method: Method): KmFunction? {
61-
val signature = method.toSignature()
62-
return functions.find { it.signature == signature }
63-
}
64-
6577
internal class CompanionObject(
6678
declaringClass: Class<*>,
6779
companionObject: String
@@ -81,8 +93,4 @@ internal class JmClass(
8193
return kmClass.functions.find { it.signature == signature }
8294
}
8395
}
84-
85-
companion object {
86-
fun createOrNull(clazz: Class<*>): JmClass? = clazz.toKmClass()?.let { JmClass(clazz, it) }
87-
}
8896
}

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

Lines changed: 17 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@ package io.github.projectmapk.jackson.module.kogera
22

33
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
44
import com.fasterxml.jackson.databind.util.LRUMap
5-
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ConstructorValueCreator
6-
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.MethodValueCreator
7-
import io.github.projectmapk.jackson.module.kogera.deser.value_instantiator.creator.ValueCreator
85
import io.github.projectmapk.jackson.module.kogera.ser.ValueClassBoxConverter
96
import java.io.Serializable
10-
import java.lang.reflect.Constructor
11-
import java.lang.reflect.Executable
12-
import java.lang.reflect.Method
137
import java.util.Optional
148

159
internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
1610
companion object {
1711
// Increment is required when properties that use LRUMap are changed.
1812
@Suppress("ConstPropertyName")
19-
private const val serialVersionUID = 1L
13+
private const val serialVersionUID = 2L
2014
}
2115

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

2619
// Initial size is 0 because the value class is not always used
2720
private val valueClassReturnTypeCache: LRUMap<AnnotatedMethod, Optional<Class<*>>> =
@@ -32,60 +25,24 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
3225
private val valueClassBoxConverterCache: LRUMap<Class<*>, ValueClassBoxConverter<*, *>> =
3326
LRUMap(0, reflectionCacheSize)
3427

35-
init {
36-
// The current default value of reflectionCacheSize is 512.
37-
// If less than 512 is specified, initialEntries shall be half of the reflectionCacheSize,
38-
// since it is assumed that the situation does not require a large amount of space from the beginning.
39-
// Conversely, if reflectionCacheSize is increased,
40-
// the amount of cache is considered to be a performance bottleneck,
41-
// and therefore reflectionCacheSize is used as is for initialEntries.
42-
val initialEntries = if (reflectionCacheSize <= KotlinModule.Builder.reflectionCacheSizeDefault) {
43-
reflectionCacheSize / 2
44-
} else {
45-
reflectionCacheSize
46-
}
47-
creatorCache = LRUMap(initialEntries, reflectionCacheSize)
48-
}
49-
5028
fun getJmClass(clazz: Class<*>): JmClass? {
51-
val optional = classCache.get(clazz)
52-
53-
return if (optional != null) {
54-
optional
55-
} else {
56-
val value = Optional.ofNullable(JmClass.createOrNull(clazz))
29+
return classCache[clazz] ?: run {
30+
val kmClass = clazz.toKmClass() ?: return null
31+
32+
// Do not parse super class for interfaces.
33+
val superJmClass = if (!clazz.isInterface) {
34+
clazz.superclass
35+
?.takeIf { it != Any::class.java } // Stop parsing when `Object` is reached
36+
?.let { getJmClass(it) }
37+
} else {
38+
null
39+
}
40+
val interfaceJmClasses = clazz.interfaces.mapNotNull { getJmClass(it) }
41+
42+
val value = JmClass(clazz, kmClass, superJmClass, interfaceJmClasses)
5743
(classCache.putIfAbsent(clazz, value) ?: value)
58-
}.orElse(null)
59-
}
60-
61-
/**
62-
* return null if declaringClass is not kotlin class
63-
*/
64-
fun valueCreatorFromJava(creator: Executable): ValueCreator<*>? = when (creator) {
65-
is Constructor<*> -> {
66-
creatorCache.get(creator)
67-
?: run {
68-
getJmClass(creator.declaringClass)?.let {
69-
val value = ConstructorValueCreator(creator, it)
70-
creatorCache.putIfAbsent(creator, value) ?: value
71-
}
72-
}
7344
}
74-
75-
is Method -> {
76-
creatorCache.get(creator)
77-
?: run {
78-
getJmClass(creator.declaringClass)?.let {
79-
val value = MethodValueCreator<Any?>(creator, it)
80-
creatorCache.putIfAbsent(creator, value) ?: value
81-
}
82-
}
83-
}
84-
85-
else -> throw IllegalStateException(
86-
"Expected a constructor or method to create a Kotlin object, instead found ${creator.javaClass.name}"
87-
)
88-
} // we cannot reflect this method so do the default Java-ish behavior
45+
}
8946

9047
private fun AnnotatedMethod.getValueClassReturnType(): Class<*>? {
9148
val getter = this.member.apply {

0 commit comments

Comments
 (0)