Polishing.

Add reactive type translation to Coroutine methods to return the expected type for AOP processing.

See #2926
This commit is contained in:
Mark Paluch
2023-09-12 15:51:49 +02:00
parent 110756a40a
commit d5cd46c1d5
2 changed files with 87 additions and 12 deletions

View File

@@ -15,12 +15,15 @@
*/
package org.springframework.data.repository.core.support;
import kotlin.Unit;
import kotlin.reflect.KFunction;
import kotlinx.coroutines.flow.Flow;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
@@ -54,25 +57,63 @@ abstract class RepositoryMethodInvoker {
private final Class<?> returnedType;
private final Invokable invokable;
private final boolean suspendedDeclaredMethod;
private final boolean returnsReactiveType;
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
protected RepositoryMethodInvoker(Method method, Invokable invokable) {
this.method = method;
this.invokable = invokable;
if (KotlinDetector.isKotlinReflectPresent()) {
this.suspendedDeclaredMethod = KotlinReflectionUtils.isSuspend(method);
this.returnedType = this.suspendedDeclaredMethod ? KotlinReflectionUtils.getReturnType(method)
: method.getReturnType();
// special case for most query methods: These can return Flux but we don't want to fail later on if the method
// is void.
if (suspendedDeclaredMethod) {
this.invokable = args -> {
Object result = invokable.invoke(args);
if (returnedType == Unit.class) {
if (result instanceof Mono<?> m) {
return m.then();
}
Flux<?> flux = result instanceof Flux<?> f ? f : ReactiveWrapperConverters.toWrapper(result, Flux.class);
return flux.then();
}
if (returnedType != Flow.class) {
if (result instanceof Mono<?> m) {
return m;
}
Flux<?> flux = result instanceof Flux<?> f ? f : ReactiveWrapperConverters.toWrapper(result, Flux.class);
if (Collection.class.isAssignableFrom(returnedType)) {
return flux.collectList();
}
return flux.singleOrEmpty();
}
return result;
};
} else {
this.invokable = invokable;
}
} else {
this.suspendedDeclaredMethod = false;
this.returnedType = method.getReturnType();
this.invokable = invokable;
}
this.returnsReactiveType = ReactiveWrappers.supports(returnedType);
}
static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) {
@@ -154,7 +195,7 @@ abstract class RepositoryMethodInvoker {
interface Invokable {
@Nullable
Object invoke(Object[] args) throws ReflectiveOperationException;
Object invoke(Object[] args) throws Exception;
}
/**
@@ -214,8 +255,6 @@ abstract class RepositoryMethodInvoker {
*/
private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker {
private final CoroutineAdapterInformation adapterInformation;
public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) {
this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance,
baseClassMethod);
@@ -236,13 +275,12 @@ abstract class RepositoryMethodInvoker {
return AopUtils.invokeJoinpointUsingReflection(instance, baseClassMethod, invocationArguments);
}
return AopUtils.invokeJoinpointUsingReflection(instance, baseClassMethod, args);
} catch (RuntimeException e) {
} catch (Exception e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
this.adapterInformation = adapterInformation;
}
/**

View File

@@ -210,6 +210,22 @@ class CoroutineCrudRepositoryUnitTests {
Mockito.verify(invocationListener).afterInvocation(captor.capture())
}
@Test // DATACMNS-1508, DATACMNS-1764
fun shouldBridgeFluxQueryMethod() {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null, any()))).thenReturn(Flux.just(sample))
val result = runBlocking {
coRepository.findOne("foo")
}
assertThat(result).isNotNull().isEqualTo(sample)
val captor = ArgumentCaptor.forClass(RepositoryMethodInvocationListener.RepositoryMethodInvocation::class.java)
Mockito.verify(invocationListener).afterInvocation(captor.capture())
}
@Test // DATACMNS-1508
fun shouldBridgeRxJavaQueryMethod() {
@@ -283,13 +299,20 @@ class CoroutineCrudRepositoryUnitTests {
val sample = User()
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null, any()))).thenReturn(Mono.just(listOf(sample)), Mono.empty<User>())
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null, any())))
.thenReturn(Mono.just(listOf(sample)), Flux.just(sample, sample), Mono.empty<User>())
val result = runBlocking {
val result1 = runBlocking {
coRepository.findSuspendedAsList("foo")
}
assertThat(result).hasSize(1).containsOnly(sample)
assertThat(result1).hasSize(1).containsOnly(sample)
val result2 = runBlocking {
coRepository.findSuspendedAsList("foo")
}
assertThat(result2).hasSize(2).contains(sample)
val emptyResult = runBlocking {
coRepository.findSuspendedAsList("foo")
@@ -298,6 +321,18 @@ class CoroutineCrudRepositoryUnitTests {
assertThat(emptyResult).isNull()
}
@Test // DATACMNS-1802
fun shouldDiscardResult() {
Mockito.`when`(factory.queryOne.execute(any())).thenReturn(Flux.empty<User>())
val result = runBlocking {
coRepository.someDelete("foo")
}
assertThat(result).isInstanceOf(Unit.javaClass)
}
interface MyCoRepository : CoroutineCrudRepository<User, String> {
suspend fun findOne(id: String): User
@@ -307,5 +342,7 @@ class CoroutineCrudRepositoryUnitTests {
suspend fun findSuspendedMultiple(id: String): Flow<User>
suspend fun findSuspendedAsList(id: String): List<User>
suspend fun someDelete(id: String)
}
}