`BridgeMethodResolver` does not resolve bridge methods for multi-level generic Kotlin suspend overrides

Author: kylequeraCreated Aug 10, 2026Updated Sep 25, 2026
Labelsstatus: waiting-for-triagein: coretheme: kotlin

Description

BridgeMethodResolver.findBridgedMethod leaves a synthetic bridge method unresolved for a generic Kotlin suspend override when the class hierarchy contains two non-bridge implementations of that method.

This affects Spring AOP in two ways:

  1. With a class-based proxy, invoking the method through a supertype reference throws AopInvocationException before a transaction starts.
  2. With an interface-based proxy, the invocation succeeds but the @Transactional attribute is not applied.

The equivalent hierarchy with only one implementation resolves correctly.

Minimal sample

Reproducer: https://github.com/kylequera/spring-suspend-bridge-repro

The sample uses only Spring Framework, Kotlin, coroutines, and a counting ReactiveTransactionManager. It requires no database and no Spring Boot application.

Run:

bash
GRADLE_USER_HOME="$PWD/.gradle-home" \
  ./gradlew test -PspringVersion=7.0.8 --no-daemon

The hierarchy is intentionally domain-neutral:

kotlin
interface RootValue
interface SpecializedValue : RootValue
class SampleValue : SpecializedValue

interface Processor<T : RootValue> {
    suspend fun <S : T> process(value: S): S
}

abstract class SpecializedProcessor<T : SpecializedValue> : Processor<T> {
    override suspend fun <S : T> process(value: S): S = value
}

open class TransactionalProcessor : SpecializedProcessor<SampleValue>() {
    @Transactional
    override suspend fun <S : SampleValue> process(value: S): S =
        super.process(value)
}

TransactionalProcessor contains the real process(SampleValue, Continuation) method and synthetic bridges for process(SpecializedValue, Continuation) and process(RootValue, Continuation).

Expected behavior

BridgeMethodResolver.findBridgedMethod should resolve either synthetic bridge to the corresponding non-bridge TransactionalProcessor.process method. Spring AOP should consequently find the transaction attribute and invoke the Kotlin suspending function correctly, independent of whether the call is made through the concrete class or the generic interface.

Actual behavior

For the RootValue bridge, BridgeMethodResolver.findBridgedMethod(bridge) returns an equal bridge method whose isBridge property is still true.

With class-based proxying, a call through Processor<SampleValue> fails with this exception chain:

org.springframework.aop.AopInvocationException:
  AOP configuration seems to be invalid: tried calling method
  [public java.lang.Object example.bridge.TransactionalProcessor.process(
  example.bridge.RootValue, kotlin.coroutines.Continuation)] on target
  [example.bridge.TransactionalProcessor]
Caused by: java.lang.IllegalArgumentException:
  Failed to get Kotlin function for method:
  public java.lang.Object example.bridge.TransactionalProcessor.process(
  example.bridge.RootValue, kotlin.coroutines.Continuation)

The counting transaction manager records zero transaction starts.

With interface-based proxying, the same call returns successfully, but the counting transaction manager again records zero transaction starts and zero commits.

The single-implementation control resolves its bridge to the non-bridge method and records one transaction start and one commit.

Analysis

resolveBridgeMethod finds more than one non-bridge candidate, so it cannot use the single-candidate shortcut and calls searchCandidates.

checkResolvedTypeMatch resolves the Kotlin-declared return type to the specialized value type, then compares it with the candidate suspend method's JVM return type, which is Object. The return-type check rejects each candidate. Since the generic parameter signatures at the two implementation levels also differ, the same-signature fallback does not select a candidate, and the original bridge is returned.

This appears to be the multi-implementation counterpart of #33045. That issue's single-implementation case is fixed, but the candidate-search path used here remains affected. Kotlin reflection does not expose synthetic bridge methods as Kotlin functions; see KT-20768.

Tested versions

  • Spring Framework 6.2.15
  • Spring Framework 6.2.19
  • Spring Framework 7.0.8
  • Kotlin 2.1.0
  • Gradle 8.10.2
  • JDK 21

The behavior is identical across all three tested Spring Framework versions.

Source: spring-projects/spring-framework