Skip to content

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

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

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

Conversation

@gregjotau

@gregjotau gregjotau commented Sep 19, 2026 •

Copy link
Copy Markdown

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

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

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

…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 #3544

Signed-off-by: Greg Taube <gregjotau@gmail.com>
@gregjotau
gregjotau force-pushed the avoid-kotlin-reflection-in-is-suspend branch from 1c32dda to c2b4082 Compare September 19, 2026 05:32
@gregjotau

Copy link
Copy Markdown
Author

Handing this over to the team: please feel free to take the change from here in whatever form suits you (team commit, different shape, or a different guard). We don't need authorship or ownership of the code; what we care about is the startup cost, so use it as you see fit.

We have tested it thoroughly against reai.no, a large Spring Boot Kotlin accounting application, with the profiles and numbers in the description above. The signed-off commit stays reachable at refs/pull/3545/head after I remove my fork, and the description contains everything needed to reproduce the measurements.

@gregjotau gregjotau closed this by deleting the head repository Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

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