Skip to content

Commit d87f1fc

Browse files
authored
Merge pull request #166 from ProjectMapK/develop
For release 2.15.3-beta6
2 parents d52ebf5 + dbee515 commit d87f1fc

File tree

20 files changed

+406
-227
lines changed

20 files changed

+406
-227
lines changed

.github/workflows/test-main.yml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,11 @@ jobs:
4444
- name: '1.8.22'
4545
version: '1.8.22'
4646
k2: false
47-
- name: '1.9.10'
48-
version: '1.9.10'
47+
- name: '1.9.20'
48+
version: '1.9.20'
4949
k2: false
50-
- name: '1.9.10 K2'
51-
version: '1.9.10'
52-
k2: true
53-
- name: '1.9.20-Beta'
54-
version: '1.9.20-Beta'
55-
k2: false
56-
- name: '1.9.20-Beta K2'
57-
version: '1.9.20-Beta'
50+
- name: '1.9.20 K2'
51+
version: '1.9.20'
5852
k2: true
5953
env:
6054
KOTLIN_VERSION: ${{ matrix.kotlin.version }}

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ plugins {
1010

1111
// Since group cannot be obtained by generateKogeraVersion, it is defined as a constant.
1212
val groupStr = "io.github.projectmapk"
13-
val jacksonVersion = "2.15.2"
13+
val jacksonVersion = "2.15.3"
1414
val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin"
1515

1616
group = groupStr
17-
version = "${jacksonVersion}-beta5"
17+
version = "${jacksonVersion}-beta6"
1818

1919
repositories {
2020
mavenCentral()

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,6 @@ internal fun Constructor<*>.toSignature(): JvmMethodSignature =
5454
internal fun Method.toSignature(): JvmMethodSignature =
5555
JvmMethodSignature(this.name, parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString())
5656

57-
private val Class<*>.descriptor: String
58-
get() = when {
59-
isPrimitive -> primitiveClassToDesc.getValue(this).toString()
60-
isArray -> "[${componentType.descriptor}"
61-
else -> "L${this.descName()};"
62-
}
63-
6457
internal fun List<KmValueParameter>.hasVarargParam(): Boolean =
6558
lastOrNull()?.let { it.varargElementType != null } ?: false
6659

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
5353
*
5454
* With this disabled, the default, collections which are typed to disallow null members (e.g. `List<String>`)
5555
* may contain null values after deserialization.
56-
* Enabling it protects against this but has performance impact.
56+
* Enabling it protects against this, but it impairs performance a bit.
57+
*
58+
* Also, if contentNulls are custom from findSetterInfo in AnnotationIntrospector, there may be a conflict.
5759
*/
5860
StrictNullChecks(enabledByDefault = false),
5961

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

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,30 @@ import io.github.projectmapk.jackson.module.kogera.ser.serializers.KotlinSeriali
2020
import java.util.*
2121

2222
/**
23-
* @param reflectionCacheSize Default: 512. Size, in items, of the caches used for mapping objects.
24-
* @param nullToEmptyCollection Default: false. Whether to deserialize null values for collection properties as
25-
* empty collections.
26-
* @param nullToEmptyMap Default: false. Whether to deserialize null values for a map property to an empty
27-
* map object.
28-
* @param nullIsSameAsDefault Default false. Whether to treat null values as absent when deserializing, thereby
29-
* using the default value provided in Kotlin.
30-
* @param singletonSupport Default: DISABLED. Mode for singleton handling.
31-
* See {@link io.github.projectmapk.jackson.module.kogera.SingletonSupport label}
32-
* @param strictNullChecks Default: false. Whether to check deserialized collections. With this disabled,
33-
* the default, collections which are typed to disallow null members
34-
* (e.g. List<String>) may contain null values after deserialization. Enabling it
35-
* 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].
23+
* @param initialCacheSize
24+
* Default: [Builder.DEFAULT_CACHE_SIZE]. See [Builder.withInitialCacheSize] for details.
25+
* @param maxCacheSize
26+
* Default: [Builder.DEFAULT_CACHE_SIZE]. See [Builder.withMaxCacheSize] for details.
27+
* @param nullToEmptyCollection
28+
* Default: false. See [KotlinFeature.NullToEmptyCollection] for details.
29+
* @param nullToEmptyMap
30+
* Default: false. See [KotlinFeature.NullToEmptyMap] for details.
31+
* @param nullIsSameAsDefault
32+
* Default: false. See [KotlinFeature.NullIsSameAsDefault] for details.
33+
* @param singletonSupport
34+
* Default: false. See [KotlinFeature.SingletonSupport] for details.
35+
* @param strictNullChecks
36+
* Default: false. See [KotlinFeature.StrictNullChecks] for details.
37+
* @param copySyntheticConstructorParameterAnnotations
38+
* Default false. See [KotlinFeature.CopySyntheticConstructorParameterAnnotations] for details.
39+
* @param useJavaDurationConversion
40+
* Default: false. See [KotlinFeature.UseJavaDurationConversion] for details.
3841
*/
3942
// Do not delete default arguments,
4043
// as this will cause an error during initialization by Spring's Jackson2ObjectMapperBuilder.
4144
public class KotlinModule private constructor(
42-
public val reflectionCacheSize: Int = Builder.reflectionCacheSizeDefault,
45+
public val initialCacheSize: Int = Builder.DEFAULT_CACHE_SIZE,
46+
public val maxCacheSize: Int = Builder.DEFAULT_CACHE_SIZE,
4347
public val nullToEmptyCollection: Boolean = NullToEmptyCollection.enabledByDefault,
4448
public val nullToEmptyMap: Boolean = NullToEmptyMap.enabledByDefault,
4549
public val nullIsSameAsDefault: Boolean = NullIsSameAsDefault.enabledByDefault,
@@ -50,7 +54,8 @@ public class KotlinModule private constructor(
5054
public val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault
5155
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
5256
private constructor(builder: Builder) : this(
53-
builder.reflectionCacheSize,
57+
builder.initialCacheSize,
58+
builder.maxCacheSize,
5459
builder.isEnabled(NullToEmptyCollection),
5560
builder.isEnabled(NullToEmptyMap),
5661
builder.isEnabled(NullIsSameAsDefault),
@@ -66,8 +71,26 @@ public class KotlinModule private constructor(
6671
)
6772
public constructor() : this(Builder())
6873

74+
init {
75+
checkMaxCacheSize(maxCacheSize)
76+
checkCacheSize(initialCacheSize, maxCacheSize)
77+
}
78+
6979
public companion object {
70-
private const val serialVersionUID = 1L
80+
@Suppress("ConstPropertyName")
81+
private const val serialVersionUID = 2L
82+
83+
private fun checkMaxCacheSize(maxCacheSize: Int) {
84+
if (maxCacheSize < 16) throw IllegalArgumentException("16 or higher must be specified")
85+
}
86+
87+
private fun checkCacheSize(initialCacheSize: Int, maxCacheSize: Int) {
88+
if (maxCacheSize < initialCacheSize) {
89+
throw IllegalArgumentException(
90+
"maxCacheSize($maxCacheSize) was less than initialCacheSize($initialCacheSize)."
91+
)
92+
}
93+
}
7194
}
7295

7396
override fun setupModule(context: SetupContext) {
@@ -79,7 +102,7 @@ public class KotlinModule private constructor(
79102
)
80103
}
81104

82-
val cache = ReflectionCache(reflectionCacheSize)
105+
val cache = ReflectionCache(initialCacheSize, maxCacheSize)
83106

84107
context.addValueInstantiators(
85108
KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault)
@@ -112,16 +135,38 @@ public class KotlinModule private constructor(
112135

113136
public class Builder {
114137
public companion object {
115-
internal const val reflectionCacheSizeDefault = 512
138+
internal const val DEFAULT_CACHE_SIZE = 512
116139
}
117140

118-
public var reflectionCacheSize: Int = reflectionCacheSizeDefault
141+
public var initialCacheSize: Int = DEFAULT_CACHE_SIZE
142+
private set
143+
public var maxCacheSize: Int = DEFAULT_CACHE_SIZE
119144
private set
120145

121146
private val bitSet: BitSet = KotlinFeature.defaults
122147

123-
public fun withReflectionCacheSize(reflectionCacheSize: Int): Builder = apply {
124-
this.reflectionCacheSize = reflectionCacheSize
148+
/**
149+
* @throws IllegalArgumentException A value less than [maxCacheSize] was entered for [initialCacheSize].
150+
*/
151+
public fun withInitialCacheSize(initialCacheSize: Int): Builder = apply {
152+
checkCacheSize(initialCacheSize, maxCacheSize)
153+
154+
this.initialCacheSize = initialCacheSize
155+
}
156+
157+
/**
158+
* Kogera's internal processing requires a certain cache size.
159+
* The lower limit of [maxCacheSize] is set to 16 for extreme use cases,
160+
* but it is recommended to set it to 100 or more unless there is a very clear reason.
161+
*
162+
* @throws IllegalArgumentException Specified size was less than the 16.
163+
* @throws IllegalArgumentException A value less than [initialCacheSize] was entered for maxCacheSize.
164+
*/
165+
public fun withMaxCacheSize(maxCacheSize: Int): Builder = apply {
166+
checkMaxCacheSize(maxCacheSize)
167+
checkCacheSize(initialCacheSize, maxCacheSize)
168+
169+
this.maxCacheSize = maxCacheSize
125170
}
126171

127172
public fun enable(feature: KotlinFeature): Builder = apply {
Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,53 @@
11
package io.github.projectmapk.jackson.module.kogera
22

3-
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
43
import com.fasterxml.jackson.databind.util.LRUMap
54
import java.io.Serializable
65
import java.lang.reflect.Method
76
import java.util.Optional
87

9-
internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
8+
// For ease of testing, maxCacheSize is limited only in KotlinModule.
9+
internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Serializable {
1010
companion object {
1111
// Increment is required when properties that use LRUMap are changed.
1212
@Suppress("ConstPropertyName")
13-
private const val serialVersionUID = 3L
13+
private const val serialVersionUID = 4L
1414
}
1515

16-
// This cache is used for both serialization and deserialization, so reserve a larger size from the start.
17-
private val classCache = LRUMap<Class<*>, JmClass>(reflectionCacheSize, reflectionCacheSize)
16+
/**
17+
* For frequently used JmClass and BoxedReturnType, reduce overhead by using Class and Method directly as key.
18+
* For other caches, if the key type overlaps, wrap it.
19+
*/
20+
private sealed class OtherCacheKey<K : Any, V : Any> {
21+
abstract val key: K
1822

19-
// Initial size is 0 because the value class is not always used
20-
private val valueClassReturnTypeCache: LRUMap<Method, Optional<Class<*>>> =
21-
LRUMap(0, reflectionCacheSize)
23+
// The comparison was implemented directly because the decompiled results showed subtle efficiency.
2224

23-
// TODO: Consider whether the cache size should be reduced more,
24-
// since the cache is used only twice locally at initialization per property.
25-
private val valueClassBoxConverterCache: LRUMap<Class<*>, ValueClassBoxConverter<*, *>> =
26-
LRUMap(0, reflectionCacheSize)
27-
private val valueClassUnboxConverterCache: LRUMap<Class<*>, ValueClassUnboxConverter<*>> =
28-
LRUMap(0, reflectionCacheSize)
25+
final override fun equals(other: Any?): Boolean =
26+
(other as? OtherCacheKey<*, *>)?.let { it::class == this::class && it.key == key } ?: false
27+
28+
// If the hashCode matches the raw key, the search efficiency is reduced, so it is displaced.
29+
final override fun hashCode(): Int = key.hashCode() * 31
30+
final override fun toString(): String = key.toString()
31+
32+
class ValueClassBoxConverter(
33+
override val key: Class<*>
34+
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter<*, *>>()
35+
class ValueClassUnboxConverter(
36+
override val key: Class<*>
37+
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*>>()
38+
}
39+
40+
private val cache = LRUMap<Any, Any>(initialCacheSize, maxCacheSize)
41+
private fun <T : Any> find(key: Any): T? = cache[key]?.let {
42+
@Suppress("UNCHECKED_CAST")
43+
it as T
44+
}
45+
46+
@Suppress("UNCHECKED_CAST")
47+
private fun <T : Any> putIfAbsent(key: Any, value: T): T = cache.putIfAbsent(key, value) as T? ?: value
2948

3049
fun getJmClass(clazz: Class<*>): JmClass? {
31-
return classCache[clazz] ?: run {
50+
return find(clazz) ?: run {
3251
val metadata = clazz.getAnnotation(Metadata::class.java) ?: return null
3352

3453
// Do not parse super class for interfaces.
@@ -43,7 +62,7 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
4362
val interfaceJmClasses = clazz.interfaces.mapNotNull { getJmClass(it) }
4463

4564
val value = JmClass(clazz, metadata, superJmClass, interfaceJmClasses)
46-
(classCache.putIfAbsent(clazz, value) ?: value)
65+
putIfAbsent(clazz, value)
4766
}
4867
}
4968

@@ -56,34 +75,29 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
5675
}
5776

5877
// Return boxed type on Kotlin for unboxed getters
59-
fun findBoxedReturnType(getter: AnnotatedMethod): Class<*>? {
60-
val method = getter.member
61-
val optional = valueClassReturnTypeCache.get(method)
78+
fun findBoxedReturnType(getter: Method): Class<*>? {
79+
val optional = find<Optional<Class<*>>>(getter)
6280

6381
return if (optional != null) {
6482
optional
6583
} else {
6684
// If the return value of the getter is a value class,
6785
// it will be serialized properly without doing anything.
6886
// TODO: Verify the case where a value class encompasses another value class.
69-
if (method.returnType.isUnboxableValueClass()) return null
87+
if (getter.returnType.isUnboxableValueClass()) return null
7088

71-
val value = Optional.ofNullable(method.getValueClassReturnType())
72-
(valueClassReturnTypeCache.putIfAbsent(method, value) ?: value)
89+
val value = Optional.ofNullable(getter.getValueClassReturnType())
90+
putIfAbsent(getter, value)
7391
}.orElse(null)
7492
}
7593

76-
fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> =
77-
valueClassBoxConverterCache.get(valueClass) ?: run {
78-
val value = ValueClassBoxConverter(unboxedClass, valueClass)
79-
80-
(valueClassBoxConverterCache.putIfAbsent(valueClass, value) ?: value)
81-
}
82-
83-
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> =
84-
valueClassUnboxConverterCache.get(valueClass) ?: run {
85-
val value = ValueClassUnboxConverter(valueClass)
94+
fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> {
95+
val key = OtherCacheKey.ValueClassBoxConverter(valueClass)
96+
return find(key) ?: putIfAbsent(key, ValueClassBoxConverter(unboxedClass, valueClass))
97+
}
8698

87-
(valueClassUnboxConverterCache.putIfAbsent(valueClass, value) ?: value)
88-
}
99+
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> {
100+
val key = OtherCacheKey.ValueClassUnboxConverter(valueClass)
101+
return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter(valueClass))
102+
}
89103
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation/JsonUnbox.kt renamed to src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation/JsonKUnbox.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ package io.github.projectmapk.jackson.module.kogera.annotation
99
@Retention(AnnotationRetention.RUNTIME)
1010
@MustBeDocumented
1111
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY_GETTER)
12-
public annotation class JsonUnbox
12+
public annotation class JsonKUnbox

0 commit comments

Comments
 (0)