We are trying to upgrade to Spring Boot 3.2 (Spring Framework 6.1) from Spring Boot 3.1.5 (Spring Framework 6.0).
We are using kotlin coroutines with r2dbc and are now getting the following stacktrace when running one of our tests
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at org.springframework.core.CoroutinesUtils.invokeSuspendingFunction(CoroutinesUtils.java:106)
at org.springframework.aop.support.AopUtils$KotlinDelegate.invokeSuspendingFunction(AopUtils.java:377)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:352)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717)
at uk.co.greenthistle.coroutinespybeanexample.ExampleRepository$MockitoMock$1lj1b0fD$$SpringCGLIB$$0.findById(<generated>)
at uk.co.greenthistle.coroutinespybeanexample.ExampleService.createVisitMapping$suspendImpl(ExampleService.kt:8)
The test has the following field
@SpyBean
lateinit var exampleRepository: ExampleRepository
which, if removed, causes the test to pass. This test also passes if we downgrade back to Spring Boot 3.1.5 and Spring Framework 6.0.
The repository is an interface with parent org.springframework.data.repository.kotlin.CoroutineCrudRepository
so therefore all functions have suspend
.
Please see example project demonstrating the issue at https://github.com/petergphillips/spring-coroutine-spybean-example
Comment From: sdeleuze
With Spring Framework 6.0, there was no support for Coroutines in AopUtils
so the implementation created with Mockito was not raising any error, but was not necessarily behaving as expected, since as far as I know, Mockito has no support for Coroutines, only Mockk has.
Spring Framework 6.1 has now Coroutines support in AopUtils
and so expect a Kotlin function with proper Coroutines implementation to be available when resolving the Java Method
via Kotlin reflection, and I suspect it is not the case with Mockito hence the failure.
I will be happy to make the error message more user friendly, but I am not sure I can do something else here since Mockito has no support for Coroutines.
Do you agree with my analysis? Any chance you could check with Mockk if you reproduce or not?
Comment From: petergphillips
I've never used mockk, but we do use org.mockito.kotlin:mockito-kotlin
support. We haven't found any issues with the support so far and enables us to mock in all the normal ways. In my example project I'd removed all occurrences of calling the spybean to keep it as simple as possible, but
I've now enhanced the example project on a 315-spybean-examples branch with examples.
Please see the integration test for examples of
1. verifying a spy bean is called with the right parameters
verify(exampleRepository).save(check { assertThat(it.nomisId).isEqualTo(1234) })
- mocking results from the repository
whenever(exampleRepository.findById(any()))
.thenReturn(VisitId(2345, "ABCD", "2022-01-01", MappingType.MIGRATED))
Note in both cases we wrap with runBlocking {}
in order to get a coroutine scope. The tests also work if we wrap in the newer kotlinx.coroutines.test.runTest
too. Obviously changing the build.gradle.kts
on the branch to 3.2.0 then means that both tests then fail.
Comment From: AlexOmarov
Got same thing - mockito-kotlin in a coroutine-based project with quite a lot of tests to be rewritten in mockk.
For now the problem may be mitigated by using a static mocking on org.springframework.aop.support.AopUtils
class and replacing invokeJoinpointUsingReflection
with it's old version.
But this isn't a solution to a problem, just a hack to get things fast.
That's how I did it:
-
Added mockk dependency to build.gradle:
testImplementation("io.mockk:mockk:1.13.8")
-
Added
BeforeEach
andAfterEach
methods to test class, which will rollinvokeJoinpointUsingReflection
method ofAopUtils
class back to it's old 6.0 state.
@BeforeEach
@Suppress("ThrowsCount")
fun setup() {
mockkStatic(AopUtils::class)
every { AopUtils.invokeJoinpointUsingReflection(any(), any(), any()) }.answers {
it.invocation.args
val method = it.invocation.args[1] as Method
val args = it.invocation.args[2] as Array<*>
val target = it.invocation.args[0]
try {
ReflectionUtils.makeAccessible(method)
method.invoke(target, *args)
} catch (ex: InvocationTargetException) {
throw ex.targetException
} catch (ex: IllegalArgumentException) {
throw AopInvocationException(
"AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex
)
} catch (ex: IllegalAccessException) {
throw AopInvocationException("Could not access method [$method]", ex)
}
}
}
@AfterEach
fun cleanUp() {
unmockkAll()
}
````
Then I used `SpyBean` annotation as in earlier versions. Didn't catch any differences so far. BUT some things should be said concerning this approach:
- firstly, to set static mocking properly i had to use mockk. Mixing mockito and mockk isn't a good idea. It will be more convenient to use Mockito for static mocking.
- secondly, mocking static methods that are deeply integrated into the framework is by all means a bad idea, and can lead to unpredictable consequences. So I hope that proper solution to this problem will be found some day :)
**Comment From: sdeleuze**
After having a deeper look, I suspect `mockito-kotlin` has a bug that prevents `ReflectJvmMapping.getKotlinFunction(method)` to find the Kotlin function from the Java method in `CoroutinesUtils#invokeSuspendingFunction`.
While debugging, I saw that the parameter type of the function mocked by `mockito-kotlin` is `Object` instead of `Long` as expected (see screenshot below), which in turn trigger this exception on Spring side.

I would suggest to raise a related issue https://github.com/mockito/mockito-kotlin to confirm. Could you please check of your side and create this issue on Mockito side?
**Comment From: sdeleuze**
I am closing this issue on Spring side, as I don't see any actionable item on our side and am reasonably confident it is a https://github.com/mockito/mockito-kotlin issue where this bug should be reported.
**Comment From: petergphillips**
Thanks for the investigation, I will raise it with mockito. There is certainly something odd going on. The spy that gets created by Mockito has two methods with the same name
```java
public java.lang.Object uk.co.greenthistle.coroutinespybeanexample.ExampleRepository$MockitoMock$ihVJzvFu.findById(java.lang.Long,kotlin.coroutines.Continuation)
and also
public java.lang.Object uk.co.greenthistle.coroutinespybeanexample.ExampleRepository$MockitoMock$ihVJzvFu.findById(java.lang.Object,kotlin.coroutines.Continuation)
whereas the original repository has only the second one (taking java.lang.Object
).
For those that are interested - I've also managed to workaround the issue by creating the spy manually in the test instead of using SpyBean
- see 320-spybean-workaround branch. I've written a setup
method that does:
@BeforeEach
fun setup() {
repositorySpy = mock(defaultAnswer = AdditionalAnswers.delegatesTo(exampleRepository))
exampleService.exampleRepository = repositorySpy
}
The spy still seems to be created with the two methods incorrectly, but not sure if it because it is using bytebuddy instead, but CoroutinesUtils
now gets called with the java.lang.Object
method and succeeds.
Comment From: koenpunt
I'm seeing similar behavior, but I'm not using mocking;
Previously this method;
override suspend fun get(source: Merchant, environment: DataFetchingEnvironment): List<ConfigurationOptionGroup>
// the interface method;
suspend fun get(source: T, environment: DataFetchingEnvironment): List<ConfigurationOptionGroup>
Was invoked just fine on a class loaded as bean, but with Spring 6.1 the method signature AopUtils trying to invoke is;
public java.lang.Object o.t.g.c.MerchantConfigurationOptionManager.get(java.lang.Object,graphql.schema.DataFetchingEnvironment,kotlin.coroutines.Continuation)
Which I guess fails because the first argument of Object
doesn't match with Merchant
of the suspend fun.
Shouldn't the framework also consider a method signature with a more specific type, when looking for methods with an argument of type Object
?
Comment From: sdeleuze
Please create a dedicated new issue with a reproducer (attached archive or link to a repository) in order to allow us to have a deeper look.