Skip to content

Commit 6b4b19f

Browse files
authored
Merge pull request #885 from k163377/719_snc
Performance improvement of `strictNullChecks`
2 parents 4ecdac2 + f9a41b6 commit 6b4b19f

File tree

11 files changed

+410
-24
lines changed

11 files changed

+410
-24
lines changed

pom.xml

+3-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,9 @@
254254
<exclude>com.fasterxml.jackson.module.kotlin.KotlinModule#getSingletonSupport()</exclude>
255255
<exclude>com.fasterxml.jackson.module.kotlin.SingletonSupport</exclude>
256256
<!-- internal -->
257-
257+
<exclude>
258+
com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector#KotlinNamesAnnotationIntrospector(com.fasterxml.jackson.module.kotlin.ReflectionCache,boolean)
259+
</exclude>
258260
</excludes>
259261
</parameter>
260262
</configuration>

release-notes/CREDITS-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Contributors:
1818
# 2.19.0 (not yet released)
1919

2020
WrongWrong (@k163377)
21+
* #885: Performance improvement of strictNullChecks
2122
* #884: Changed the base class of MissingKotlinParameterException to InvalidNullException
2223
* #878: Fix for #876
2324
* #868: Added test case for FAIL_ON_NULL_FOR_PRIMITIVES

release-notes/VERSION-2.x

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Co-maintainers:
1717
------------------------------------------------------------------------
1818

1919
2.19.0 (not yet released)
20+
#885: A new `StrictNullChecks` option(KotlinFeature.NewStrictNullChecks) has been added which greatly improves throughput.
21+
Benchmarks show a consistent throughput drop of less than 2% when enabled (prior to the improvement, the worst throughput drop was more than 30%).
22+
Note that the new backend changes the exception thrown to `InvalidNullException` and with it the error message.
23+
Also note that the base class for `MissingKotlinParameterException` was changed to `InvalidNullException` in #884.
2024
#884: The base class for `MissingKotlinParameterException` has been changed to `InvalidNullException`.
2125
If you do not catch this exception or catch `MismatchedInputException`, the behavior is unchanged.
2226
If you are catching both `MismatchedKotlinParameterException` and `InvalidNullException`, you must catch `MismatchedKotlinParameterException` first.

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.fasterxml.jackson.module.kotlin
22

3+
import com.fasterxml.jackson.annotation.JsonSetter
4+
import com.fasterxml.jackson.databind.exc.InvalidNullException
35
import java.util.BitSet
46

57
/**
@@ -40,6 +42,11 @@ enum class KotlinFeature(internal val enabledByDefault: Boolean) {
4042
* may contain null values after deserialization.
4143
* Enabling it protects against this but has significant performance impact.
4244
*/
45+
@Deprecated(
46+
level = DeprecationLevel.WARNING,
47+
message = "This option will be migrated to the new backend in 2.21.",
48+
replaceWith = ReplaceWith("NewStrictNullChecks")
49+
)
4350
StrictNullChecks(enabledByDefault = false),
4451

4552
/**
@@ -66,7 +73,23 @@ enum class KotlinFeature(internal val enabledByDefault: Boolean) {
6673
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
6774
* See [jackson-module-kotlin#651] for details.
6875
*/
69-
UseJavaDurationConversion(enabledByDefault = false);
76+
UseJavaDurationConversion(enabledByDefault = false),
77+
78+
/**
79+
* New [StrictNullChecks] feature with improved throughput.
80+
* Internally, it will be the same as if [JsonSetter] (contentNulls = FAIL) had been granted.
81+
* Benchmarks show that it can check for illegal nulls with throughput nearly identical to the default (see [jackson-module-kotlin#719]).
82+
*
83+
* Note that in the new backend, the exception thrown has changed from [MissingKotlinParameterException] to [InvalidNullException].
84+
* The message will be changed accordingly.
85+
* Since 2.19, the base class of [MissingKotlinParameterException] has also been changed to [InvalidNullException],
86+
* so be careful when catching it.
87+
*
88+
* This is a temporary option for a phased backend migration,
89+
* which will eventually be merged into [StrictNullChecks].
90+
* Also, specifying both this and [StrictNullChecks] is not permitted.
91+
*/
92+
NewStrictNullChecks(enabledByDefault = false);
7093

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

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
77
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
88
import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport
99
import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks
10+
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks
1011
import com.fasterxml.jackson.module.kotlin.KotlinFeature.KotlinPropertyNameAsImplicitName
1112
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
1213
import java.util.*
@@ -42,9 +43,10 @@ class KotlinModule private constructor(
4243
val nullToEmptyMap: Boolean = NullToEmptyMap.enabledByDefault,
4344
val nullIsSameAsDefault: Boolean = NullIsSameAsDefault.enabledByDefault,
4445
val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
45-
val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
46+
strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
4647
val kotlinPropertyNameAsImplicitName: Boolean = KotlinPropertyNameAsImplicitName.enabledByDefault,
4748
val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault,
49+
private val newStrictNullChecks: Boolean = NewStrictNullChecks.enabledByDefault,
4850
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
4951
/*
5052
* Prior to 2.18, an older Enum called SingletonSupport was used to manage feature.
@@ -64,6 +66,19 @@ class KotlinModule private constructor(
6466
)
6567
val enabledSingletonSupport: Boolean get() = singletonSupport
6668

69+
private val oldStrictNullChecks: Boolean = strictNullChecks
70+
71+
// To reduce the amount of destructive changes, no properties will be added to the public.
72+
val strictNullChecks: Boolean = if (strictNullChecks) {
73+
if (newStrictNullChecks) {
74+
throw IllegalArgumentException("Enabling both StrictNullChecks and NewStrictNullChecks is not permitted.")
75+
}
76+
77+
true
78+
} else {
79+
newStrictNullChecks
80+
}
81+
6782
companion object {
6883
// Increment when option is added
6984
private const val serialVersionUID = 3L
@@ -84,6 +99,7 @@ class KotlinModule private constructor(
8499
builder.isEnabled(StrictNullChecks),
85100
builder.isEnabled(KotlinPropertyNameAsImplicitName),
86101
builder.isEnabled(UseJavaDurationConversion),
102+
builder.isEnabled(NewStrictNullChecks),
87103
)
88104

89105
override fun setupModule(context: SetupContext) {
@@ -95,7 +111,7 @@ class KotlinModule private constructor(
95111

96112
val cache = ReflectionCache(reflectionCacheSize)
97113

98-
context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, strictNullChecks))
114+
context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, oldStrictNullChecks))
99115

100116
if (singletonSupport) {
101117
context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier)
@@ -109,7 +125,9 @@ class KotlinModule private constructor(
109125
nullIsSameAsDefault,
110126
useJavaDurationConversion
111127
))
112-
context.appendAnnotationIntrospector(KotlinNamesAnnotationIntrospector(cache, kotlinPropertyNameAsImplicitName))
128+
context.appendAnnotationIntrospector(
129+
KotlinNamesAnnotationIntrospector(cache, newStrictNullChecks, kotlinPropertyNameAsImplicitName)
130+
)
113131

114132
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
115133
context.addKeyDeserializers(KotlinKeyDeserializers)

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.fasterxml.jackson.module.kotlin
22

33
import com.fasterxml.jackson.annotation.JsonProperty
4+
import com.fasterxml.jackson.annotation.JsonSetter
5+
import com.fasterxml.jackson.annotation.Nulls
46
import com.fasterxml.jackson.databind.JavaType
57
import com.fasterxml.jackson.databind.cfg.MapperConfig
68
import com.fasterxml.jackson.databind.introspect.Annotated
@@ -12,8 +14,10 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
1214
import com.fasterxml.jackson.databind.introspect.PotentialCreator
1315
import java.lang.reflect.Constructor
1416
import java.util.Locale
17+
import kotlin.collections.getOrNull
1518
import kotlin.reflect.KClass
1619
import kotlin.reflect.KFunction
20+
import kotlin.reflect.KParameter
1721
import kotlin.reflect.full.hasAnnotation
1822
import kotlin.reflect.full.memberProperties
1923
import kotlin.reflect.full.primaryConstructor
@@ -22,6 +26,7 @@ import kotlin.reflect.jvm.javaType
2226

2327
internal class KotlinNamesAnnotationIntrospector(
2428
private val cache: ReflectionCache,
29+
private val strictNullChecks: Boolean,
2530
private val kotlinPropertyNameAsImplicitName: Boolean
2631
) : NopAnnotationIntrospector() {
2732
private fun getterNameFromJava(member: AnnotatedMethod): String? {
@@ -73,16 +78,26 @@ internal class KotlinNamesAnnotationIntrospector(
7378
}
7479

7580
override fun refineDeserializationType(config: MapperConfig<*>, a: Annotated, baseType: JavaType): JavaType =
76-
(a as? AnnotatedParameter)?.let { _ ->
77-
cache.findKotlinParameter(a)?.let { param ->
78-
val rawType = a.rawType
79-
(param.type.classifier as? KClass<*>)
80-
?.java
81-
?.takeIf { it.isUnboxableValueClass() && it != rawType }
82-
?.let { config.constructType(it) }
83-
}
81+
findKotlinParameter(a)?.let { param ->
82+
val rawType = a.rawType
83+
(param.type.classifier as? KClass<*>)
84+
?.java
85+
?.takeIf { it.isUnboxableValueClass() && it != rawType }
86+
?.let { config.constructType(it) }
8487
} ?: baseType
8588

89+
override fun findSetterInfo(ann: Annotated): JsonSetter.Value = ann.takeIf { strictNullChecks }
90+
?.let { _ ->
91+
findKotlinParameter(ann)?.let { param ->
92+
if (param.requireStrictNullCheck(ann.type)) {
93+
JsonSetter.Value.forContentNulls(Nulls.FAIL)
94+
} else {
95+
null
96+
}
97+
}
98+
}
99+
?: super.findSetterInfo(ann)
100+
86101
override fun findDefaultCreator(
87102
config: MapperConfig<*>,
88103
valueClass: AnnotatedClass,
@@ -106,8 +121,18 @@ internal class KotlinNamesAnnotationIntrospector(
106121
}
107122

108123
private fun findKotlinParameterName(param: AnnotatedParameter): String? = cache.findKotlinParameter(param)?.name
124+
125+
private fun findKotlinParameter(param: Annotated) = (param as? AnnotatedParameter)
126+
?.let { cache.findKotlinParameter(it) }
109127
}
110128

129+
private fun KParameter.markedNonNullAt(index: Int) = type.arguments.getOrNull(index)?.type?.isMarkedNullable == false
130+
131+
private fun KParameter.requireStrictNullCheck(type: JavaType): Boolean =
132+
((type.isArrayType || type.isCollectionLikeType) && this.markedNonNullAt(0)) ||
133+
(type.isMapLikeType && this.markedNonNullAt(1))
134+
135+
111136
// If it is not a Kotlin class or an Enum, Creator is not used
112137
private fun AnnotatedClass.creatableKotlinClass(): KClass<*>? = annotated
113138
.takeIf { it.isKotlinClass() && !it.isEnum }

src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault
66
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
77
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
88
import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport
9-
import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks
9+
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks
1010
import org.junit.jupiter.api.Assertions.assertNotNull
1111
import org.junit.jupiter.api.Test
1212
import kotlin.test.assertEquals
@@ -35,7 +35,7 @@ class DslTest {
3535
enable(NullToEmptyMap)
3636
enable(NullIsSameAsDefault)
3737
enable(SingletonSupport)
38-
enable(StrictNullChecks)
38+
enable(NewStrictNullChecks)
3939
}
4040

4141
assertNotNull(module)

src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt

+15
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,24 @@ import org.junit.jupiter.api.Assertions.assertEquals
66
import org.junit.jupiter.api.Assertions.assertFalse
77
import org.junit.jupiter.api.Assertions.assertTrue
88
import org.junit.jupiter.api.Test
9+
import org.junit.jupiter.api.assertThrows
910
import kotlin.test.assertNotNull
1011

1112
class KotlinModuleTest {
13+
// After the final migration is complete, this test will be removed.
14+
@Test
15+
fun strictNullChecksTests() {
16+
assertTrue(kotlinModule { enable(StrictNullChecks) }.strictNullChecks)
17+
assertTrue(kotlinModule { enable(NewStrictNullChecks) }.strictNullChecks)
18+
19+
assertThrows<IllegalArgumentException> {
20+
kotlinModule {
21+
enable(StrictNullChecks)
22+
enable(NewStrictNullChecks)
23+
}
24+
}
25+
}
26+
1227
@Test
1328
fun builder_Defaults() {
1429
val module = KotlinModule.Builder().build()

0 commit comments

Comments
 (0)