Skip to content

Avoid Kotlin reflection for coroutine method detection without a Continuation parameter - #3556

Closed
gregjotau wants to merge 1 commit into
spring-projects:mainfrom
beint-no:avoid-kotlin-reflection-in-is-suspend
Closed

gregjotau wants to merge 1 commit into
spring-projects:mainfrom
beint-no:avoid-kotlin-reflection-in-is-suspend

Conversation

@gregjotau

@gregjotau gregjotau commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Recreates #3545 as requested after its source fork was deleted. This applies the original signed-off change unchanged to current main. I consent to this change being merged.

KotlinReflectionUtils.isSuspend(Method) resolves the KFunction for every Kotlin method through ReflectJvmMapping.getKotlinFunction(method), which materializes all members of the declaring KClass via Kotlin reflection, only to read KFunction.isSuspend(). AbstractRepositoryMetadata.getReturnType(Method) calls it for every query method during repository initialization, and RepositoryMethodInvoker calls it again per method.

A suspend function always compiles to a JVM method whose last parameter is kotlin.coroutines.Continuation, so KotlinDetector.isSuspendingFunction(method) (already used by QueryExecutionResultHandler in this module) can rule out regular methods without loading Kotlin reflection. Methods that do declare a trailing Continuation parameter are still verified through KFunction.isSuspend(), so a non-suspending function that takes a Continuation argument explicitly keeps returning false.

RepositoryMethodInvoker.CoroutineAdapterInformation.create(...) had the same pattern (two findKotlinFunction lookups used only for isSuspend()); it now delegates to KotlinReflectionUtils.isSuspend(...).

Measurements from the original submission

Cold-JVM micro-benchmark calling isSuspend for the 1,418 non-synthetic methods of the 337 Kotlin repository interfaces of a Spring Boot 4.2.0-SNAPSHOT application (JDK 27, Apple M5 Max, 5 runs each):

main This change
isSuspend over 1,418 methods 1,146–1,341 ms 3–4 ms

In a wall-clock startup profile of the same application (async-profiler, 2 ms sampling, extracted Boot layout, 243 JPA repositories), KotlinReflectionUtils.isSuspend accounted for 241 of 8,694 main-thread samples (2.8 % of startup), all below ReflectJvmMapping.getKotlinFunction; with this change the frame no longer appears. Part of the KClass member computation is shared with other Kotlin reflection users during startup, so the end-to-end saving depends on what else touches those classes.

Verification from the original submission

KotlinReflectionUtilsSuspendUnitTests, KotlinReflectionUtilsUnitTests, RepositoryMethodInvokerUnitTests and both CoroutineRepositoryMetadataUnitTests pass locally. I also verified the fragment path with a throwaway MockK-free variant of CoroutineCrudRepositoryCustomImplementationUnitTests (suspend fragment method, regular fragment method, and suspend base methods bridging to a reactive backing repository including Flow and Unit results). CoroutineCrudRepositoryUnitTests and CoroutineCrudRepositoryCustomImplementationUnitTests could not run on my machine on either main or this branch because the MockK agent fails to self-attach there, so CI will have to cover those.

Closes #3544

Verification on current main

The restored patch has the same stable Git patch ID as the original commit. ./mvnw -Dtest=KotlinReflectionUtilsSuspendUnitTests,KotlinReflectionUtilsUnitTests,RepositoryMethodInvokerUnitTests,CoroutineRepositoryMetadataUnitTests test completed successfully on JDK 27: 29 tests, 0 failures, 0 errors, 4 skipped (all in KotlinReflectionUtilsUnitTests). The other 25 tests passed, including both coroutine metadata test classes.

…s without a Continuation parameter.

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 spring-projects#3544

Signed-off-by: Greg Taube <gregjotau@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 5, 2026
@mp911de mp911de self-assigned this Oct 6, 2026
@mp911de mp911de added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 6, 2026
@mp911de mp911de changed the title Avoid Kotlin reflection in KotlinReflectionUtils.isSuspend for methods without a Continuation parameter Avoid Kotlin reflection for coroutine method detection without a Continuation parameter Oct 6, 2026
mp911de pushed a commit that referenced this pull request Oct 6, 2026
…tinuation` parameter.

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
Original pull request: #3556
Signed-off-by: Greg Taube <gregjotau@gmail.com>
mp911de added a commit that referenced this pull request Oct 6, 2026
More compact formatting. Refactor isSearchQuery to lazy computation for fewer isSuspend calls.

See #3544
Original pull request: #3556
mp911de pushed a commit that referenced this pull request Oct 6, 2026
…tinuation` parameter.

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
Original pull request: #3556
Signed-off-by: Greg Taube <gregjotau@gmail.com>
mp911de added a commit that referenced this pull request Oct 6, 2026
More compact formatting. Refactor isSearchQuery to lazy computation for fewer isSuspend calls.

See #3544
Original pull request: #3556
@mp911de

mp911de commented Oct 6, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution. That's merged, polished, and backported now.

@mp911de mp911de closed this Oct 6, 2026
@mp911de mp911de added this to the 4.1.2 (2026.0.2) milestone Oct 6, 2026
@gregjotau
gregjotau deleted the avoid-kotlin-reflection-in-is-suspend branch October 6, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid Kotlin reflection in KotlinReflectionUtils.isSuspend for methods without a Continuation parameter

3 participants