Skip to content

Commit ef5c047

Browse files
authored
Merge pull request #137 from ProjectMapK/develop
For release 2.15.2-beta3
2 parents fdf7601 + 05be7d7 commit ef5c047

File tree

21 files changed

+461
-162
lines changed

21 files changed

+461
-162
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This project has the following features compared to `jackson-module-kotlin`.
1010
- More `Kotlin` friendly behavior
1111

1212
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)
13+
![](https://docs.google.com/spreadsheets/d/e/2PACX-1vSDpaOENd0a-qO_zK7C5_UkSxEKk7BxLjmyg8XVnPP0jj6J5rgoA8cCnm_lj7lflx6NDjvC1yMUPrce/pubchart?oid=1594997844&format=image)
1414

1515
This project is experimental, but passes all the tests implemented in `jackson-module-kotlin` except for the intentional incompatibility.
1616

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ val jacksonVersion = "2.15.2"
1414
val generatedSrcPath = "$buildDir/generated/kotlin"
1515

1616
group = groupStr
17-
version = "${jacksonVersion}-beta0"
17+
version = "${jacksonVersion}-beta3"
1818

1919
repositories {
2020
mavenCentral()
@@ -33,6 +33,7 @@ dependencies {
3333
testImplementation("io.mockk:mockk:1.13.3")
3434

3535
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
36+
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
3637
}
3738

3839
kotlin {

docs/KogeraSpecificImplementations.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@ In `jackson-module-kogera`, there are several intentional design changes that ar
22
This page summarizes them.
33

44
# Common
5-
## Set `Kotlin Property` as positive and ignore functions.
6-
In `jackson-module-kotlin`, functions with getterLike or setterLike names are handled in the same way as `Kotlin Property`.
7-
On the other hand, this implementation causes discrepancies between the `Kotlin` description and the processing results by `Jackson`.
8-
9-
Therefore, `kogera` processes only `Kotlin Property` and excludes other functions from processing.
10-
In addition, `kogera` uses the content defined in `Kotlin` as its name.
11-
12-
These changes make it impossible to manipulate the results using `JvmName`.
13-
145
## Change of `hasRequiredMarker` specification
156
The `KotlinAnnotationIntrospector::hasRequiredMarker` function in `jackson-module-kotlin` has several problems.
167
In `kogera`, the specification has been revised as follows.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.github.projectmapk.jackson.module.kogera
2+
3+
import com.fasterxml.jackson.databind.JavaType
4+
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
5+
import com.fasterxml.jackson.databind.type.TypeFactory
6+
import com.fasterxml.jackson.databind.util.StdConverter
7+
8+
/**
9+
* A converter that only performs box processing for the value class.
10+
* Note that constructor-impl is not called.
11+
* @param S is nullable because value corresponds to a nullable value class.
12+
* see [io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinFallbackAnnotationIntrospector.findNullSerializer]
13+
*/
14+
internal class ValueClassBoxConverter<S : Any?, D : Any>(
15+
unboxedClass: Class<S>,
16+
val valueClass: Class<D>
17+
) : StdConverter<S, D>() {
18+
private val boxMethod = valueClass.getDeclaredMethod("box-impl", unboxedClass).apply {
19+
if (!this.isAccessible) this.isAccessible = true
20+
}
21+
22+
@Suppress("UNCHECKED_CAST")
23+
override fun convert(value: S): D = boxMethod.invoke(null, value) as D
24+
25+
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
26+
}
27+
28+
internal class ValueClassUnboxConverter<T : Any>(val valueClass: Class<T>) : StdConverter<T, Any?>() {
29+
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
30+
if (!this.isAccessible) this.isAccessible = true
31+
}
32+
val unboxedClass: Class<*> = unboxMethod.returnType
33+
34+
override fun convert(value: T): Any? = unboxMethod.invoke(value)
35+
36+
override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
37+
override fun getOutputType(typeFactory: TypeFactory): JavaType =
38+
typeFactory.constructType(unboxMethod.genericReturnType)
39+
40+
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
41+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import kotlinx.metadata.KmClass
66
import kotlinx.metadata.KmClassifier
77
import kotlinx.metadata.KmType
88
import kotlinx.metadata.KmValueParameter
9-
import kotlinx.metadata.jvm.JvmFieldSignature
109
import kotlinx.metadata.jvm.JvmMethodSignature
1110
import kotlinx.metadata.jvm.KotlinClassMetadata
1211
import java.lang.reflect.AnnotatedElement
1312
import java.lang.reflect.Constructor
1413
import java.lang.reflect.Field
1514
import java.lang.reflect.Method
1615

16+
internal typealias JavaDuration = java.time.Duration
17+
internal typealias KotlinDuration = kotlin.time.Duration
18+
1719
internal fun Class<*>.isUnboxableValueClass() = this.getAnnotation(JvmInline::class.java) != null
1820

1921
internal fun Class<*>.toKmClass(): KmClass? = this.getAnnotation(Metadata::class.java)
@@ -67,8 +69,7 @@ private val Class<*>.descriptor: String
6769
else -> "L${this.descName()};"
6870
}
6971

70-
internal fun Field.toSignature(): JvmFieldSignature =
71-
JvmFieldSignature(this.name, this.type.descriptor)
72+
internal fun Field.desc(): String = this.type.descriptor
7273

7374
internal fun List<KmValueParameter>.hasVarargParam(): Boolean =
7475
lastOrNull()?.let { it.varargElementType != null } ?: false

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import java.lang.reflect.Method
1515

1616
// Jackson Metadata Class
1717
internal class JmClass(
18-
private val clazz: Class<*>,
18+
clazz: Class<*>,
1919
kmClass: KmClass,
2020
superJmClass: JmClass?,
2121
interfaceJmClasses: List<JmClass>
@@ -47,15 +47,15 @@ internal class JmClass(
4747

4848
fun findKmConstructor(constructor: Constructor<*>): KmConstructor? {
4949
val descHead = constructor.parameterTypes.toDescBuilder()
50-
val desc = CharArray(descHead.length + 1).apply {
51-
descHead.getChars(0, descHead.length, this, 0)
52-
this[this.lastIndex] = 'V'
50+
val len = descHead.length
51+
val desc = CharArray(len + 1).apply {
52+
descHead.getChars(0, len, this, 0)
53+
this[len] = 'V'
5354
}.let { String(it) }
5455

5556
// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
5657
val valueDesc = descHead
57-
.deleteCharAt(descHead.length - 1)
58-
.append("Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
58+
.replace(len - 1, len, "Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
5959
.toString()
6060

6161
// Constructors always have the same name, so only desc is compared
@@ -67,7 +67,7 @@ internal class JmClass(
6767

6868
// Field name always matches property name
6969
fun findPropertyByField(field: Field): KmProperty? = allPropsMap[field.name]
70-
?.takeIf { it.fieldSignature == field.toSignature() }
70+
?.takeIf { it.fieldSignature?.desc == field.desc() }
7171

7272
fun findPropertyByGetter(getter: Method): KmProperty? {
7373
val signature = getter.toSignature()
@@ -81,7 +81,7 @@ internal class JmClass(
8181
private val companionField: Field = declaringClass.getDeclaredField(companionObject)
8282
val type: Class<*> = companionField.type
8383
val isAccessible: Boolean = companionField.isAccessible
84-
private val kmClass: KmClass by lazy { type.toKmClass()!! }
84+
private val functions by lazy { type.toKmClass()!!.functions }
8585
val instance: Any by lazy {
8686
// To prevent the call from failing, save the initial value and then rewrite the flag.
8787
if (!companionField.isAccessible) companionField.isAccessible = true
@@ -90,7 +90,7 @@ internal class JmClass(
9090

9191
fun findFunctionByMethod(method: Method): KmFunction? {
9292
val signature = method.toSignature()
93-
return kmClass.functions.find { it.signature == signature }
93+
return functions.find { it.signature == signature }
9494
}
9595
}
9696
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,16 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
7373
*
7474
* @see KotlinClassIntrospector
7575
*/
76-
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false);
76+
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false),
77+
78+
/**
79+
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
80+
*
81+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
82+
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
83+
* See [jackson-module-kotlin#651] for details.
84+
*/
85+
UseJavaDurationConversion(enabledByDefault = false);
7786

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

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyColl
88
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyMap
99
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.SingletonSupport
1010
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.StrictNullChecks
11+
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.UseJavaDurationConversion
1112
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinFallbackAnnotationIntrospector
1213
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinPrimaryAnnotationIntrospector
1314
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.KotlinDeserializers
@@ -32,6 +33,8 @@ import java.util.*
3233
* the default, collections which are typed to disallow null members
3334
* (e.g. List<String>) may contain null values after deserialization. Enabling it
3435
* protects against this but has significant performance impact.
36+
* @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
37+
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
3538
*/
3639
// Do not delete default arguments,
3740
// as this will cause an error during initialization by Spring's Jackson2ObjectMapperBuilder.
@@ -43,7 +46,8 @@ public class KotlinModule private constructor(
4346
public val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
4447
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
4548
public val copySyntheticConstructorParameterAnnotations: Boolean =
46-
CopySyntheticConstructorParameterAnnotations.enabledByDefault
49+
CopySyntheticConstructorParameterAnnotations.enabledByDefault,
50+
public val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault
4751
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
4852
private constructor(builder: Builder) : this(
4953
builder.reflectionCacheSize,
@@ -52,7 +56,8 @@ public class KotlinModule private constructor(
5256
builder.isEnabled(NullIsSameAsDefault),
5357
builder.isEnabled(SingletonSupport),
5458
builder.isEnabled(StrictNullChecks),
55-
builder.isEnabled(CopySyntheticConstructorParameterAnnotations)
59+
builder.isEnabled(CopySyntheticConstructorParameterAnnotations),
60+
builder.isEnabled(UseJavaDurationConversion)
5661
)
5762

5863
@Deprecated(
@@ -87,16 +92,18 @@ public class KotlinModule private constructor(
8792
context.insertAnnotationIntrospector(
8893
KotlinPrimaryAnnotationIntrospector(nullToEmptyCollection, nullToEmptyMap, cache)
8994
)
90-
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(strictNullChecks, cache))
95+
context.appendAnnotationIntrospector(
96+
KotlinFallbackAnnotationIntrospector(strictNullChecks, useJavaDurationConversion, cache)
97+
)
9198

9299
if (copySyntheticConstructorParameterAnnotations) {
93100
context.setClassIntrospector(KotlinClassIntrospector)
94101
}
95102

96-
context.addDeserializers(KotlinDeserializers(cache))
103+
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
97104
context.addKeyDeserializers(KotlinKeyDeserializers)
98-
context.addSerializers(KotlinSerializers())
99-
context.addKeySerializers(KotlinKeySerializers())
105+
context.addSerializers(KotlinSerializers(cache))
106+
context.addKeySerializers(KotlinKeySerializers(cache))
100107

101108
// ranges
102109
context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java)

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ 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.ser.ValueClassBoxConverter
65
import java.io.Serializable
76
import java.util.Optional
87

98
internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
109
companion object {
1110
// Increment is required when properties that use LRUMap are changed.
1211
@Suppress("ConstPropertyName")
13-
private const val serialVersionUID = 2L
12+
private const val serialVersionUID = 3L
1413
}
1514

1615
// This cache is used for both serialization and deserialization, so reserve a larger size from the start.
@@ -24,16 +23,19 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
2423
// since the cache is used only twice locally at initialization per property.
2524
private val valueClassBoxConverterCache: LRUMap<Class<*>, ValueClassBoxConverter<*, *>> =
2625
LRUMap(0, reflectionCacheSize)
26+
private val valueClassUnboxConverterCache: LRUMap<Class<*>, ValueClassUnboxConverter<*>> =
27+
LRUMap(0, reflectionCacheSize)
2728

2829
fun getJmClass(clazz: Class<*>): JmClass? {
2930
return classCache[clazz] ?: run {
3031
val kmClass = clazz.toKmClass() ?: return null
3132

3233
// Do not parse super class for interfaces.
3334
val superJmClass = if (!clazz.isInterface) {
34-
clazz.superclass
35-
?.takeIf { it != Any::class.java } // Stop parsing when `Object` is reached
36-
?.let { getJmClass(it) }
35+
clazz.superclass?.let {
36+
// Stop parsing when `Object` is reached
37+
if (it != Any::class.java) getJmClass(it) else null
38+
}
3739
} else {
3840
null
3941
}
@@ -75,4 +77,11 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
7577

7678
(valueClassBoxConverterCache.putIfAbsent(valueClass, value) ?: value)
7779
}
80+
81+
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> =
82+
valueClassUnboxConverterCache.get(valueClass) ?: run {
83+
val value = ValueClassUnboxConverter(valueClass)
84+
85+
(valueClassUnboxConverterCache.putIfAbsent(valueClass, value) ?: value)
86+
}
7887
}

0 commit comments

Comments
 (0)