From c2b4082f5bc53f80bce0aa1a4e9157aa8098d3da Mon Sep 17 00:00:00 2001 From: Greg Taube Date: Sat, 19 Sep 2026 07:03:11 +0200 Subject: [PATCH] Avoid Kotlin reflection in KotlinReflectionUtils.isSuspend for methods without a Continuation parameter. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Kotlin suspend function always compiles to a JVM method whose last parameter is kotlin.coroutines.Continuation. Check for that parameter with KotlinDetector.isSuspendingFunction(…) before resolving the KFunction, so that regular Kotlin methods no longer materialize all members of their declaring class through Kotlin reflection. Methods that declare a trailing Continuation parameter are still verified via KFunction.isSuspend(). Also let RepositoryMethodInvoker.CoroutineAdapterInformation use isSuspend(…) instead of resolving both KFunctions itself only to read isSuspend(). Closes #3544 Signed-off-by: Greg Taube --- .../core/support/RepositoryMethodInvoker.java | 13 +--- .../data/util/KotlinReflectionUtils.java | 8 ++- .../KotlinReflectionUtilsSuspendUnitTests.kt | 69 +++++++++++++++++++ 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 src/test/kotlin/org/springframework/data/util/KotlinReflectionUtilsSuspendUnitTests.kt diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index 267833da52..2daf34669b 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -16,7 +16,6 @@ package org.springframework.data.repository.core.support; import kotlin.Unit; -import kotlin.reflect.KFunction; import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -46,6 +45,7 @@ * * @author Mark Paluch * @author Christoph Strobl + * @author Greg Taube * @since 2.4 * @see #forFragmentMethod(Method, Object, Method) * @see #forRepositoryQuery(Method, RepositoryQuery) @@ -319,15 +319,8 @@ public static CoroutineAdapterInformation create(Method declaredMethod, Method b baseClassMethod.getParameterCount()); } - KFunction declaredFunction = KotlinDetector.isKotlinType(declaredMethod.getDeclaringClass()) - ? KotlinReflectionUtils.findKotlinFunction(declaredMethod) - : null; - KFunction baseClassFunction = KotlinDetector.isKotlinType(baseClassMethod.getDeclaringClass()) - ? KotlinReflectionUtils.findKotlinFunction(baseClassMethod) - : null; - - boolean suspendedDeclaredMethod = declaredFunction != null && declaredFunction.isSuspend(); - boolean suspendedBaseClassMethod = baseClassFunction != null && baseClassFunction.isSuspend(); + boolean suspendedDeclaredMethod = KotlinReflectionUtils.isSuspend(declaredMethod); + boolean suspendedBaseClassMethod = KotlinReflectionUtils.isSuspend(baseClassMethod); boolean reactiveBaseClassMethod = !suspendedBaseClassMethod && ReactiveWrapperConverters.supports(baseClassMethod.getReturnType()); diff --git a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java index ef0941d285..267e8ecde3 100644 --- a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java @@ -43,6 +43,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Johannes Englmeier + * @author Greg Taube * @since 2.3 * @see org.springframework.core.KotlinDetector#isKotlinReflectPresent() */ @@ -109,8 +110,11 @@ public static boolean isDataClass(Class type) { */ public static boolean isSuspend(Method method) { - KFunction invokedFunction = KotlinDetector.isKotlinType(method.getDeclaringClass()) ? findKotlinFunction(method) - : null; + if (!KotlinDetector.isSuspendingFunction(method) || !KotlinDetector.isKotlinType(method.getDeclaringClass())) { + return false; + } + + KFunction invokedFunction = findKotlinFunction(method); return invokedFunction != null && invokedFunction.isSuspend(); } diff --git a/src/test/kotlin/org/springframework/data/util/KotlinReflectionUtilsSuspendUnitTests.kt b/src/test/kotlin/org/springframework/data/util/KotlinReflectionUtilsSuspendUnitTests.kt new file mode 100644 index 0000000000..5cc8b05d2c --- /dev/null +++ b/src/test/kotlin/org/springframework/data/util/KotlinReflectionUtilsSuspendUnitTests.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026-present 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.data.util + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.data.repository.sample.User +import kotlin.coroutines.Continuation + +/** + * Unit tests for [KotlinReflectionUtils.isSuspend]. + * + * @author Greg Taube + */ +class KotlinReflectionUtilsSuspendUnitTests { + + @Test // GH-3544 + fun `detects suspend functions`() { + + val method = SuspendingInterface::class.java.getDeclaredMethod("suspending", Continuation::class.java) + + assertThat(KotlinReflectionUtils.isSuspend(method)).isTrue() + } + + @Test // GH-3544 + fun `considers Continuation parameter of a regular function as non-suspending`() { + + val method = SuspendingInterface::class.java.getDeclaredMethod("continuationParameter", Continuation::class.java) + + assertThat(KotlinReflectionUtils.isSuspend(method)).isFalse() + } + + @Test // GH-3544 + fun `considers regular functions as non-suspending`() { + + assertThat(KotlinReflectionUtils.isSuspend(SuspendingInterface::class.java.getDeclaredMethod("regular"))).isFalse() + assertThat(KotlinReflectionUtils.isSuspend(SuspendingInterface::class.java.getDeclaredMethod("withParameter", User::class.java))).isFalse() + } + + @Test // GH-3544 + fun `considers Java methods as non-suspending`() { + + assertThat(KotlinReflectionUtils.isSuspend(Object::class.java.getDeclaredMethod("toString"))).isFalse() + } + + interface SuspendingInterface { + + suspend fun suspending(): User + + fun continuationParameter(continuation: Continuation): User + + fun regular(): User + + fun withParameter(user: User): User + } +}