From 647d9fd949d1cf7928d5d4c9ec79dae1b1e0cd6d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 2 Aug 2024 10:11:34 +0200 Subject: [PATCH] Provide RepositoryMethodMetadata during method invocation. Provide context during repository method invocation to potential consumers. See: #3090 Original Pull Request: #3093 --- .../repositories/custom-implementations.adoc | 91 +++++++++++++++++++ .../DefaultRepositoryMethodMetadata.java | 79 ++++++++++++++++ .../QueryExecutorMethodInterceptor.java | 5 +- .../core/support/RepositoryComposition.java | 19 ++-- .../core/support/RepositoryMethodInvoker.java | 39 ++++++-- .../support/RepositoryMethodMetadata.java | 44 +++++++++ .../RepositoryCompositionUnitTests.java | 65 +++++++++++-- .../RepositoryMethodInvokerUnitTests.java | 29 +++--- 8 files changed, 335 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java create mode 100644 src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc index b4c84fcf0..8aa643643 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -238,6 +238,97 @@ XML:: ====== ==== +[[repositories.spring-factories]] +==== Registering Fragments with spring.factories + +As already mentioned in the <> section, the infrastructure only auto detects fragments within the repositories base package. Therefore fragments residing in another location or maybe contributed by an external archive will not be found if they do not share a common namespace. +Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section. + +Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index. + +First all you need is the fragment interface. Please note the generic `` parameter to align the fragment with the repository domain type. + +==== +[source,java] +---- +package com.acme.search; + +public interface SearchExtension { + + List search(String text, Limit limit); +} +---- +==== + +Let's assume the actual full text search is available via a `SearchService` that is registered as a `Bean` within the context so we can consume it in our `SearchExtension` implementation. All we need to run the search is the collection/index name and a object mapper that converts the search results into actual domain objects as sketched out below. + +==== +[source,java] +---- +package com.acme.search; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Limit; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata; + +class DefaultSearchExtension implements SearchExtension { + + private SearchService service; + + DefaultSearchExtension(@Autowired SearchService service) { + this.service = service; + } + + public List search(String text, Limit limit) { + return search(RepositoryMethodMetadata.get(), text, limit); + } + + List search(RepositoryMethodMetadata metadata, String text, Limit limit) { + + Class domainType = metadata.repository().getDomainType(); + + String indexName = domainType.getSimpleName().toLowerCase(); + List jsonResult = service.search(indexName, text, 0, limit.max()); + + return jsonResult.stream().map( ... ).collect(toList()); + } +} +---- +==== + +In the snipped above we use `RepositoryMethodMetadata.get()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched. + +[TIP] +==== +For testing you can use `TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata)` to provide repository method metadata. +==== + +Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're good to go. + +==== +[source,properties] +---- +com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +---- +==== + +To make use of the extension simply add the interface to the repository as shown below. The infrastructure will take care placing the required `RepositoryMethodMetadata` so all that + +==== +[source,java] +---- +package io.my.movies; + +import com.acme.search.SearchExtension; +import org.springframework.data.repository.CrudRepository; + + +public interface MovieRepository extends CrudRepository, SearchExtension { + +} +---- +==== + [[repositories.customize-base-repository]] == Customize the Base Repository diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java new file mode 100644 index 000000000..3a5b35f9a --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support; + +import java.lang.reflect.Method; + +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Christoph Strobl + */ +class DefaultRepositoryMethodMetadata implements RepositoryMethodMetadata { + + private final RepositoryMetadata repositoryMetadata; + private final MethodMetadata methodMetadata; + + DefaultRepositoryMethodMetadata(RepositoryMetadata repositoryMetadata, MethodMetadata methodMetadata) { + + this.repositoryMetadata = repositoryMetadata; + this.methodMetadata = methodMetadata; + } + + static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, + Method declaredMethod) { + + return repositoryMethodMetadata(repositoryMetadata, declaredMethod, null); + } + + static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, + Method declaredMethod, @Nullable Method targetMethod) { + + return new DefaultRepositoryMethodMetadata(repositoryMetadata, + new DefaultMethodMetadata(declaredMethod, targetMethod)); + } + + static void bind(RepositoryMethodMetadata metadata) { + TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata); + } + + static void unbind() { + TransactionSynchronizationManager.unbindResourceIfPossible(RepositoryMethodMetadata.class); + } + + @Override + public RepositoryMetadata repository() { + return repositoryMetadata; + } + + @Override + public MethodMetadata method() { + return methodMetadata; + } + + @Override + public String toString() { + return "DefaultRepositoryMethodMetadata{" + "repository=" + repositoryMetadata.getRepositoryInterface() + + ", domainType=" + repositoryMetadata.getDomainType() + ", invokedMethod=" + methodMetadata.declaredMethod() + + ", targetMethod=" + methodMetadata.targetMethod() + '}'; + } + + record DefaultMethodMetadata(Method declaredMethod, @Nullable Method targetMethod) implements MethodMetadata { + } + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java index 13309fa6a..135b0d278 100644 --- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java @@ -33,6 +33,7 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethod; @@ -163,7 +164,9 @@ class QueryExecutorMethodInterceptor implements MethodInterceptor { RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method); if (invocationMetadata == null) { - invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method)); + + DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(repositoryInformation, method); + invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(repositoryMethodMetadata, queries.get(method)); invocationMetadataCache.put(method, invocationMetadata); } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index d26dedf25..2e01b208f 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.Streamable; @@ -281,7 +282,7 @@ public class RepositoryComposition { ReflectionUtils.makeAccessible(methodToCall); - return fragments.invoke(metadata != null ? metadata.getRepositoryInterface() : method.getDeclaringClass(), listener, + return fragments.invoke(metadata, listener, method, methodToCall, argumentConverter.apply(methodToCall, args)); } @@ -369,7 +370,6 @@ public class RepositoryComposition { private final List> fragments; private RepositoryFragments(List> fragments) { - this.fragments = fragments; } @@ -382,6 +382,10 @@ public class RepositoryComposition { return EMPTY; } + public static RepositoryFragments empty(RepositoryMetadata metadata) { + return EMPTY; + } + /** * Create {@link RepositoryFragments} from just implementation objects. * @@ -484,7 +488,7 @@ public class RepositoryComposition { /** * Invoke {@link Method} by resolving the fragment that implements a suitable method. * - * @param repositoryInterface + * @param metadata * @param listener * @param invokedMethod invoked method as per invocation on the interface. * @param methodToCall backend method that is backing the call. @@ -493,7 +497,7 @@ public class RepositoryComposition { * @throws Throwable */ @Nullable - Object invoke(Class repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod, + Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMulticaster listener, Method invokedMethod, Method methodToCall, Object[] args) throws Throwable { RepositoryFragment fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment); @@ -507,12 +511,15 @@ public class RepositoryComposition { if (repositoryMethodInvoker == null) { - repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(invokedMethod, optional.get(), + DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(metadata, invokedMethod, methodToCall); + repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(repositoryMethodMetadata, optional.get(), methodToCall); + invocationMetadataCache.put(invokedMethod, repositoryMethodInvoker); } - return repositoryMethodInvoker.invoke(repositoryInterface, listener, args); + Class target = (metadata != null && metadata.getRepositoryInterface() != null) ? metadata.getRepositoryInterface() : invokedMethod.getDeclaringClass(); + return repositoryMethodInvoker.invoke(target, listener, args); } private RepositoryFragment findImplementationFragment(Method key) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index 8647ba458..e4ba94a01 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -18,12 +18,19 @@ package org.springframework.data.repository.core.support; import kotlin.Unit; import kotlin.reflect.KFunction; import kotlinx.coroutines.flow.Flow; +import org.springframework.core.type.MethodMetadata; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.TypeInformation; +import org.springframework.transaction.support.TransactionSynchronizationManager; 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.Set; import java.util.stream.Stream; import org.reactivestreams.Publisher; @@ -46,8 +53,8 @@ import org.springframework.lang.Nullable; * @author Mark Paluch * @author Christoph Strobl * @since 2.4 - * @see #forFragmentMethod(Method, Object, Method) - * @see #forRepositoryQuery(Method, RepositoryQuery) +// * @see #forFragmentMethod(Method, Object, Method) +// * @see #forRepositoryQuery(Method, RepositoryQuery) * @see RepositoryQuery * @see RepositoryComposition */ @@ -57,11 +64,13 @@ abstract class RepositoryMethodInvoker { private final Class returnedType; private final Invokable invokable; private final boolean suspendedDeclaredMethod; + protected RepositoryMethodMetadata repositoryMethodMetadata; @SuppressWarnings("ReactiveStreamsUnusedPublisher") - protected RepositoryMethodInvoker(Method method, Invokable invokable) { + protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetadata, Invokable invokable) { - this.method = method; + this.repositoryMethodMetadata = repositoryMethodMetadata; + this.method = repositoryMethodMetadata.method().declaredMethod(); if (KotlinDetector.isKotlinReflectPresent()) { @@ -116,7 +125,7 @@ abstract class RepositoryMethodInvoker { } } - static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) { + static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata declaredMethod, RepositoryQuery query) { return new RepositoryQueryMethodInvoker(declaredMethod, query); } @@ -128,7 +137,7 @@ abstract class RepositoryMethodInvoker { * @param baseMethod the base method to call on fragment {@code instance}. * @return {@link RepositoryMethodInvoker} to call a fragment {@link Method}. */ - static RepositoryMethodInvoker forFragmentMethod(Method declaredMethod, Object instance, Method baseMethod) { + static RepositoryMethodInvoker forFragmentMethod(RepositoryMethodMetadata declaredMethod, Object instance, Method baseMethod) { return new RepositoryFragmentMethodInvoker(declaredMethod, instance, baseMethod); } @@ -167,6 +176,10 @@ abstract class RepositoryMethodInvoker { try { + if(RepositoryMethodMetadata.get() == null && repositoryMethodMetadata != null) { + DefaultRepositoryMethodMetadata.bind(repositoryMethodMetadata); + } + Object result = invokable.invoke(args); if (result != null && ReactiveWrappers.supports(result.getClass())) { @@ -184,6 +197,8 @@ abstract class RepositoryMethodInvoker { } catch (Exception e) { multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e))); throw e; + } finally { + DefaultRepositoryMethodMetadata.unbind(); } } @@ -202,7 +217,7 @@ abstract class RepositoryMethodInvoker { * Implementation to invoke query methods. */ private static class RepositoryQueryMethodInvoker extends RepositoryMethodInvoker { - public RepositoryQueryMethodInvoker(Method method, RepositoryQuery repositoryQuery) { + public RepositoryQueryMethodInvoker(RepositoryMethodMetadata method, RepositoryQuery repositoryQuery) { super(method, repositoryQuery::execute); } } @@ -255,15 +270,19 @@ abstract class RepositoryMethodInvoker { */ private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker { - public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) { - this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance, + public RepositoryFragmentMethodInvoker(RepositoryMethodMetadata metadata, Object instance, Method baseClassMethod) { + this(CoroutineAdapterInformation.create(metadata.method().declaredMethod(), baseClassMethod), metadata, instance, baseClassMethod); } - public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, Method declaredMethod, + public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, RepositoryMethodMetadata declaredMethod, Object instance, Method baseClassMethod) { super(declaredMethod, args -> { + try { + + + if (adapterInformation.shouldAdaptReactiveToSuspended()) { /* * Kotlin suspended functions are invoked with a synthetic Continuation parameter that keeps track of the Coroutine context. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java new file mode 100644 index 000000000..6dd0cf178 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support; + +import java.lang.reflect.Method; + +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Christoph Strobl + */ +public interface RepositoryMethodMetadata { + + @Nullable + static RepositoryMethodMetadata get() { + return (RepositoryMethodMetadata) TransactionSynchronizationManager.getResource(RepositoryMethodMetadata.class); + } + + MethodMetadata method(); + + RepositoryMetadata repository(); + + interface MethodMetadata { + + Method declaredMethod(); + @Nullable Method targetMethod(); + } + +} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java index 8c6864e03..30322ef06 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java @@ -15,8 +15,12 @@ */ package org.springframework.data.repository.core.support; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,8 +31,11 @@ import org.springframework.data.annotation.Id; import org.springframework.data.domain.Example; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -52,8 +59,7 @@ class RepositoryCompositionUnitTests { RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( new DefaultRepositoryMetadata(PersonRepository.class), backingRepo.getClass(), RepositoryComposition.empty()); - var mixin = RepositoryFragment.implemented(QueryByExampleExecutor.class, - queryByExampleExecutor); + var mixin = RepositoryFragment.implemented(QueryByExampleExecutor.class, queryByExampleExecutor); var base = RepositoryFragment.implemented(backingRepo); @@ -139,8 +145,7 @@ class RepositoryCompositionUnitTests { assertThatExceptionOfType(FragmentNotImplementedException.class) // .isThrownBy(mixed::validateImplementation) // - .withMessageContaining( - "Fragment org.springframework.data.repository.query.QueryByExampleExecutor") + .withMessageContaining("Fragment org.springframework.data.repository.query.QueryByExampleExecutor") .withMessageContaining("has no implementation"); } @@ -165,6 +170,33 @@ class RepositoryCompositionUnitTests { .containsSequence(initial, structural); } + @Test // GH-3090 + void fragmentInvocationProvidesRepositoryMethodMetadata() throws Throwable { + + RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( + new DefaultRepositoryMetadata(CapturingRepository.class), CapturingRepository.class, + RepositoryComposition.empty()); + + MethodMetadataCapturingMixin capturingMixin = new MethodMetadataCapturingMixin(); + RepositoryFragment foo = RepositoryFragment.implemented(capturingMixin); + + var fooBar = RepositoryComposition.of(RepositoryFragments.of(foo)) + .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)).withMetadata(repositoryInformation); + + var getString = ReflectionUtils.findMethod(CapturingRepository.class, "getString"); + + assertThat(getString).isNotNull(); + fooBar.invoke(fooBar.findMethod(getString).get()); + + RepositoryMethodMetadata lastValue = capturingMixin.getLastValue(); + assertThat(lastValue.repository()).isNotNull().extracting(RepositoryMetadata::getRepositoryInterface) + .isEqualTo(CapturingRepository.class); + + // TODO: I'm actually lost on that one + // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::declaredMethod).isEqualTo(getString); + // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::targetMethod).isEqualTo(FooMixin.class.getMethod("getString")); + } + interface PersonRepository extends Repository, QueryByExampleExecutor { Person save(Person entity); @@ -204,6 +236,10 @@ class RepositoryCompositionUnitTests { } + interface CapturingRepository extends Repository, FooMixin { + + } + interface FooMixin { String getString(); @@ -218,6 +254,23 @@ class RepositoryCompositionUnitTests { } } + class MethodMetadataCapturingMixin implements FooMixin { + + List captured = new ArrayList<>(3); + + @Override + public String getString() { + + captured.add(RepositoryMethodMetadata.get()); + return FooMixinImpl.INSTANCE.getString(); + } + + @Nullable + RepositoryMethodMetadata getLastValue() { + return CollectionUtils.lastElement(captured); + } + } + interface BarMixin { String getString(); diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java index bc479f9df..e1d0d9ea4 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java @@ -15,6 +15,18 @@ */ package org.springframework.data.repository.core.support; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.reactive.ReactiveFlowKt; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Iterator; @@ -26,8 +38,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Stream; -import kotlin.coroutines.Continuation; -import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.assertj.core.api.Assertions; import org.assertj.core.data.Percentage; import org.jetbrains.annotations.NotNull; @@ -39,26 +49,18 @@ import org.mockito.internal.stubbing.answers.AnswersWithDelay; import org.mockito.internal.stubbing.answers.Returns; import org.mockito.junit.jupiter.MockitoExtension; import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.CoroutineRepositoryMetadataUnitTests.MyCoroutineRepository; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * @author Christoph Strobl * @author Johannes Englmeier @@ -316,7 +318,8 @@ class RepositoryMethodInvokerUnitTests { RepositoryMethodInvokerStub(Class repositoryInterface, RepositoryInvocationMulticaster multicaster, String methodName, Invokable invokable) { - super(methodByName(repositoryInterface, methodName), invokable); + super(DefaultRepositoryMethodMetadata.repositoryMethodMetadata(mock(RepositoryMetadata.class), methodByName(repositoryInterface, methodName)), invokable); + this.repositoryInterface = repositoryInterface; this.multicaster = multicaster; }