Skip to content

Commit 45c2104

Browse files
committed
Optimize Kotlin inline class checks
This commit fixes a performance regression caused by gh-31698, and more specifically by KClass#isValue invocations which are slow since they load the whole module to find the class to get the descriptor. After discussing with the Kotlin team, it has been decided that only checking for the presence of `@JvmInline` annotation is enough for Spring use case. As a consequence, this commit introduces a new KotlinDetector#isInlineClass method that performs such check, and BeanUtils, CoroutinesUtils and WebMVC/WebFlux InvocableHandlerMethod have been refined to leverage it. Closes gh-32334
1 parent 5830aac commit 45c2104

File tree

6 files changed

+108
-21
lines changed

6 files changed

+108
-21
lines changed

spring-beans/src/main/java/org/springframework/beans/BeanUtils.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,9 @@
3232
import java.util.Set;
3333

3434
import kotlin.jvm.JvmClassMappingKt;
35-
import kotlin.jvm.JvmInline;
3635
import kotlin.reflect.KClass;
3736
import kotlin.reflect.KFunction;
3837
import kotlin.reflect.KParameter;
39-
import kotlin.reflect.full.KAnnotatedElements;
4038
import kotlin.reflect.full.KClasses;
4139
import kotlin.reflect.jvm.KCallablesJvm;
4240
import kotlin.reflect.jvm.ReflectJvmMapping;
@@ -874,8 +872,7 @@ public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
874872
if (primaryCtor == null) {
875873
return null;
876874
}
877-
if (kClass.isValue() && !KAnnotatedElements
878-
.findAnnotations(kClass, JvmClassMappingKt.getKotlinClass(JvmInline.class)).isEmpty()) {
875+
if (KotlinDetector.isInlineClass(clazz)) {
879876
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
880877
Assert.state(constructors.length == 1,
881878
"Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor");

spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -120,11 +120,17 @@ public static Publisher<?> invokeSuspendingFunction(CoroutineContext context, Me
120120
case INSTANCE -> argMap.put(parameter, target);
121121
case VALUE, EXTENSION_RECEIVER -> {
122122
if (!parameter.isOptional() || args[index] != null) {
123-
if (parameter.getType().getClassifier() instanceof KClass<?> kClass && kClass.isValue()) {
123+
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
124124
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
125-
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
126-
Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName());
127-
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
125+
if (KotlinDetector.isInlineClass(javaClass)) {
126+
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
127+
Assert.state(methods.length == 1,
128+
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
129+
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
130+
}
131+
else {
132+
argMap.put(parameter, args[index]);
133+
}
128134
}
129135
else {
130136
argMap.put(parameter, args[index]);

spring-core/src/main/java/org/springframework/core/KotlinDetector.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,24 +35,34 @@ public abstract class KotlinDetector {
3535
@Nullable
3636
private static final Class<? extends Annotation> kotlinMetadata;
3737

38+
@Nullable
39+
private static final Class<? extends Annotation> kotlinJvmInline;
40+
3841
// For ConstantFieldFeature compliance, otherwise could be deduced from kotlinMetadata
3942
private static final boolean kotlinPresent;
4043

4144
private static final boolean kotlinReflectPresent;
4245

4346
static {
44-
Class<?> metadata;
4547
ClassLoader classLoader = KotlinDetector.class.getClassLoader();
48+
Class<?> metadata = null;
49+
Class<?> jvmInline = null;
4650
try {
4751
metadata = ClassUtils.forName("kotlin.Metadata", classLoader);
52+
try {
53+
jvmInline = ClassUtils.forName("kotlin.jvm.JvmInline", classLoader);
54+
}
55+
catch (ClassNotFoundException ex) {
56+
// JVM inline support not available
57+
}
4858
}
4959
catch (ClassNotFoundException ex) {
5060
// Kotlin API not available - no Kotlin support
51-
metadata = null;
5261
}
5362
kotlinMetadata = (Class<? extends Annotation>) metadata;
5463
kotlinPresent = (kotlinMetadata != null);
55-
kotlinReflectPresent = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader);
64+
kotlinReflectPresent = kotlinPresent && ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader);
65+
kotlinJvmInline = (Class<? extends Annotation>) jvmInline;
5666
}
5767

5868

@@ -93,4 +103,14 @@ public static boolean isSuspendingFunction(Method method) {
93103
return false;
94104
}
95105

106+
/**
107+
* Determine whether the given {@code Class} is an inline class
108+
* (annotated with {@code @JvmInline}).
109+
* @since 6.1.5
110+
* @see <a href="https://kotlinlang.org/docs/inline-classes.html">Kotlin inline value classes</a>
111+
*/
112+
public static boolean isInlineClass(Class<?> clazz) {
113+
return (kotlinJvmInline != null && clazz.getDeclaredAnnotation(kotlinJvmInline) != null);
114+
}
115+
96116
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.core
17+
18+
import org.assertj.core.api.Assertions
19+
import org.junit.jupiter.api.Test
20+
21+
/**
22+
* Kotlin tests for [KotlinDetector].
23+
*
24+
* @author Sebastien Deleuze
25+
*/
26+
class KotlinDetectorTests {
27+
28+
@Test
29+
fun isKotlinType() {
30+
Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetectorTests::class.java)).isTrue()
31+
}
32+
33+
@Test
34+
fun isNotKotlinType() {
35+
Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetector::class.java)).isFalse()
36+
}
37+
38+
@Test
39+
fun isInlineClass() {
40+
Assertions.assertThat(KotlinDetector.isInlineClass(ValueClass::class.java)).isTrue()
41+
}
42+
43+
@Test
44+
fun isNotInlineClass() {
45+
Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetector::class.java)).isFalse()
46+
Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetectorTests::class.java)).isFalse()
47+
}
48+
49+
@JvmInline
50+
value class ValueClass(val value: String)
51+
52+
}

spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,17 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
319319
case INSTANCE -> argMap.put(parameter, target);
320320
case VALUE, EXTENSION_RECEIVER -> {
321321
if (!parameter.isOptional() || args[index] != null) {
322-
if (parameter.getType().getClassifier() instanceof KClass<?> kClass && kClass.isValue()) {
322+
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
323323
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
324-
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
325-
Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName());
326-
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
324+
if (KotlinDetector.isInlineClass(javaClass)) {
325+
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
326+
Assert.state(methods.length == 1,
327+
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
328+
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
329+
}
330+
else {
331+
argMap.put(parameter, args[index]);
332+
}
327333
}
328334
else {
329335
argMap.put(parameter, args[index]);

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,17 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
330330
case INSTANCE -> argMap.put(parameter, target);
331331
case VALUE, EXTENSION_RECEIVER -> {
332332
if (!parameter.isOptional() || args[index] != null) {
333-
if (parameter.getType().getClassifier() instanceof KClass<?> kClass && kClass.isValue()) {
333+
if (parameter.getType().getClassifier() instanceof KClass<?> kClass) {
334334
Class<?> javaClass = JvmClassMappingKt.getJavaClass(kClass);
335-
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
336-
Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName());
337-
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
335+
if (KotlinDetector.isInlineClass(javaClass)) {
336+
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter);
337+
Assert.state(methods.length == 1,
338+
"Unable to find a single box-impl synthetic static method in " + javaClass.getName());
339+
argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index]));
340+
}
341+
else {
342+
argMap.put(parameter, args[index]);
343+
}
338344
}
339345
else {
340346
argMap.put(parameter, args[index]);

0 commit comments

Comments
 (0)