spring-projects / spring-projects/spring-framework
`BridgeMethodResolver` does not resolve bridge methods for multi-level generic Kotlin suspend overrides
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 60.2k
- Forks
- 38.8k
- Avg merge
- 5d 2h
- Merged PRs (30d)
- 27
Description
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:
- With a class-based proxy, invoking the method through a supertype reference
throwsAopInvocationExceptionbefore a transaction starts. - With an interface-based proxy, the invocation succeeds but the
@Transactionalattribute 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at BridgeMethodResolver.findBridgedMethod and follow resolveBridgeMethod, searchCandidates, and checkResolvedTypeMatch. Run the linked reproducer with the provided Gradle command to observe the bridge resolution and transaction behavior. Done means both synthetic bridges resolve to the non-bridge TransactionalProcessor.process method and the reproducer records the expected transaction start and commit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, kotlin, spring
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100