Skip to content

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

Description

@kylequera

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:

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

The hierarchy is intentionally domain-neutral:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: coreIssues in core modules (aop, beans, core, context, expression)status: waiting-for-triageAn issue we've not yet triaged or decided ontheme: kotlinAn issue related to Kotlin support

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions