Skip to content

Avoid Kotlin reflection in MethodParameter for non-suspending functions - #37299

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

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

Conversation

@gregjotau

@gregjotau gregjotau commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

MethodParameter.getGenericParameterType() and getParameterType() resolve the return type of a Kotlin method (parameter index -1) through KotlinDelegate, which calls ReflectJvmMapping.getKotlinFunction(method) only to check KFunction.isSuspend(). That lookup materializes all members of the declaring KClass through Kotlin reflection (metadata deserialization plus a linear scan comparing javaMethod per member) and is repeated for every method whose return type is resolved.

A suspending function always compiles to a JVM method whose last parameter is kotlin.coroutines.Continuation, which KotlinDetector.isSuspendingFunction(Method) already checks without Kotlin reflection; the rest of the framework (AOP, caching, scheduling, messaging, ConstructorResolver) relies on that check. This change uses it as a guard in KotlinDelegate.getGenericReturnType(...) and getReturnType(...), so regular Kotlin methods fall back to Method.getGenericReturnType() / Method.getReturnType() immediately. Suspending functions keep the existing KFunction based resolution, and a non-suspending function that declares a trailing Continuation parameter explicitly is still verified through KFunction.isSuspend(), so results are unchanged in every case.

Measurements

Cold-JVM micro-benchmark that constructs new MethodParameter(method, -1) and calls getGenericParameterType() and getParameterType() for the 1,418 non-synthetic methods of the 337 Kotlin repository interfaces of a large Spring Boot 4.2.0-SNAPSHOT / Framework 7.1.0-SNAPSHOT application (JDK 27, Apple M5 Max, 5 runs each):

main This change
Return type resolution, 1,418 methods 1,584–2,766 ms 57–150 ms

The baseline number includes Kotlin reflection bootstrap, which the patched path avoids entirely; in an application that touches Kotlin reflection elsewhere the saving is correspondingly smaller. In a wall-clock startup profile of that application (async-profiler, 2 ms sampling, extracted Boot layout, 243 Spring Data JPA repositories), MethodParameter$KotlinDelegate.getGenericReturnType / getReturnType accounted for 124 of 8,694 main-thread samples (1.4 % of startup), all below ReflectJvmMapping.getKotlinFunction; with this change the frames no longer appear. Spring Data's KotlinReflectionUtils.isSuspend has the same pattern; I have proposed the equivalent guard there in spring-projects/spring-data-commons#3545.

Verification

MethodParameterKotlinTests gains two tests that pin the return type resolution of a regular function and of a regular function that declares a Continuation parameter explicitly; both pass on main and on this branch. ./gradlew :spring-core:check passes on JDK 25, including the JDK 21 and JDK 24 test suites, checkstyle and architecture checks. Related to #21546.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 19, 2026
MethodParameter resolves the return type of Kotlin methods through
ReflectJvmMapping.getKotlinFunction(...) only to check whether the
function is suspending. That lookup materializes all members of the
declaring KClass via Kotlin reflection, for every method.

A suspending function always compiles to a JVM method whose last
parameter is kotlin.coroutines.Continuation, which
KotlinDetector.isSuspendingFunction(...) already checks without Kotlin
reflection. Use it as a guard so that regular Kotlin methods fall back
to plain Java reflection immediately, while suspending functions keep
the existing KFunction based return type resolution.

Signed-off-by: Greg Taube <gregjotau@gmail.com>
@gregjotau
gregjotau force-pushed the avoid-kotlin-reflection-in-method-parameter branch from 636e894 to 8e8b152 Compare September 19, 2026 05:34
@gregjotau

Copy link
Copy Markdown
Contributor 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/37299/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
@sbrannen sbrannen added theme: kotlin An issue related to Kotlin support in: core Issues in core modules (aop, beans, core, context, expression) labels Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: waiting-for-triage An issue we've not yet triaged or decided on theme: kotlin An issue related to Kotlin support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants