diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 29d1bbd9094b..cedf0408f2a3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -32,11 +32,9 @@ import java.util.Set; import kotlin.jvm.JvmClassMappingKt; -import kotlin.jvm.JvmInline; import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; -import kotlin.reflect.full.KAnnotatedElements; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; @@ -874,8 +872,7 @@ public static Constructor findPrimaryConstructor(Class clazz) { if (primaryCtor == null) { return null; } - if (kClass.isValue() && !KAnnotatedElements - .findAnnotations(kClass, JvmClassMappingKt.getKotlinClass(JvmInline.class)).isEmpty()) { + if (KotlinDetector.isInlineClass(clazz)) { Constructor[] constructors = clazz.getDeclaredConstructors(); Assert.state(constructors.length == 1, "Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor"); diff --git a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java index c97ef5001a34..b7c413da2787 100644 --- a/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java +++ b/spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,11 +120,17 @@ public static Publisher invokeSuspendingFunction(CoroutineContext context, Me case INSTANCE -> argMap.put(parameter, target); case VALUE, EXTENSION_RECEIVER -> { if (!parameter.isOptional() || args[index] != null) { - if (parameter.getType().getClassifier() instanceof KClass kClass && kClass.isValue()) { + if (parameter.getType().getClassifier() instanceof KClass kClass) { Class javaClass = JvmClassMappingKt.getJavaClass(kClass); - Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); - Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); - argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + if (KotlinDetector.isInlineClass(javaClass)) { + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); + Assert.state(methods.length == 1, + "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); + argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + } + else { + argMap.put(parameter, args[index]); + } } else { argMap.put(parameter, args[index]); diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java index fde4e10a6c91..80d8d658afe7 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,24 +35,34 @@ public abstract class KotlinDetector { @Nullable private static final Class kotlinMetadata; + @Nullable + private static final Class kotlinJvmInline; + // For ConstantFieldFeature compliance, otherwise could be deduced from kotlinMetadata private static final boolean kotlinPresent; private static final boolean kotlinReflectPresent; static { - Class metadata; ClassLoader classLoader = KotlinDetector.class.getClassLoader(); + Class metadata = null; + Class jvmInline = null; try { metadata = ClassUtils.forName("kotlin.Metadata", classLoader); + try { + jvmInline = ClassUtils.forName("kotlin.jvm.JvmInline", classLoader); + } + catch (ClassNotFoundException ex) { + // JVM inline support not available + } } catch (ClassNotFoundException ex) { // Kotlin API not available - no Kotlin support - metadata = null; } kotlinMetadata = (Class) metadata; kotlinPresent = (kotlinMetadata != null); - kotlinReflectPresent = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + kotlinReflectPresent = kotlinPresent && ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + kotlinJvmInline = (Class) jvmInline; } @@ -93,4 +103,14 @@ public static boolean isSuspendingFunction(Method method) { return false; } + /** + * Determine whether the given {@code Class} is an inline class + * (annotated with {@code @JvmInline}). + * @since 6.1.5 + * @see Kotlin inline value classes + */ + public static boolean isInlineClass(Class clazz) { + return (kotlinJvmInline != null && clazz.getDeclaredAnnotation(kotlinJvmInline) != null); + } + } diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt new file mode 100644 index 000000000000..0e5184a05d34 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.core + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +/** + * Kotlin tests for [KotlinDetector]. + * + * @author Sebastien Deleuze + */ +class KotlinDetectorTests { + + @Test + fun isKotlinType() { + Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetectorTests::class.java)).isTrue() + } + + @Test + fun isNotKotlinType() { + Assertions.assertThat(KotlinDetector.isKotlinType(KotlinDetector::class.java)).isFalse() + } + + @Test + fun isInlineClass() { + Assertions.assertThat(KotlinDetector.isInlineClass(ValueClass::class.java)).isTrue() + } + + @Test + fun isNotInlineClass() { + Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetector::class.java)).isFalse() + Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetectorTests::class.java)).isFalse() + } + + @JvmInline + value class ValueClass(val value: String) + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 11f0fcb49066..bfa90a691208 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -319,11 +319,17 @@ public static Object invokeFunction(Method method, Object target, Object[] args) case INSTANCE -> argMap.put(parameter, target); case VALUE, EXTENSION_RECEIVER -> { if (!parameter.isOptional() || args[index] != null) { - if (parameter.getType().getClassifier() instanceof KClass kClass && kClass.isValue()) { + if (parameter.getType().getClassifier() instanceof KClass kClass) { Class javaClass = JvmClassMappingKt.getJavaClass(kClass); - Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); - Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); - argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + if (KotlinDetector.isInlineClass(javaClass)) { + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); + Assert.state(methods.length == 1, + "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); + argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + } + else { + argMap.put(parameter, args[index]); + } } else { argMap.put(parameter, args[index]); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 88edf33d36a4..dbfa1585b208 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -330,11 +330,17 @@ public static Object invokeFunction(Method method, Object target, Object[] args, case INSTANCE -> argMap.put(parameter, target); case VALUE, EXTENSION_RECEIVER -> { if (!parameter.isOptional() || args[index] != null) { - if (parameter.getType().getClassifier() instanceof KClass kClass && kClass.isValue()) { + if (parameter.getType().getClassifier() instanceof KClass kClass) { Class javaClass = JvmClassMappingKt.getJavaClass(kClass); - Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); - Assert.state(methods.length == 1, "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); - argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + if (KotlinDetector.isInlineClass(javaClass)) { + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(javaClass, boxImplFilter); + Assert.state(methods.length == 1, + "Unable to find a single box-impl synthetic static method in " + javaClass.getName()); + argMap.put(parameter, ReflectionUtils.invokeMethod(methods[0], null, args[index])); + } + else { + argMap.put(parameter, args[index]); + } } else { argMap.put(parameter, args[index]);