Rename RepositoryMethodMetadata and move it to an interceptor.

Rename RepositoryMethodMetadata to RepositoryMethodContext.
Add flags to enable method metadata exposure.

See: #3090
Original Pull Request: #3093
This commit is contained in:
Mark Paluch
2024-08-06 09:10:40 +02:00
committed by Christoph Strobl
parent 647d9fd949
commit 658c04d00e
13 changed files with 296 additions and 255 deletions

View File

@@ -269,7 +269,7 @@ 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;
import org.springframework.data.repository.core.support.RepositoryMethodContext;
class DefaultSearchExtension<T> implements SearchExtension<T> {
@@ -280,12 +280,12 @@ class DefaultSearchExtension<T> implements SearchExtension<T> {
}
public List<T> search(String text, Limit limit) {
return search(RepositoryMethodMetadata.get(), text, limit);
return search(RepositoryMethodContext.currentMethod(), text, limit);
}
List<T> search(RepositoryMethodMetadata metadata, String text, Limit limit) {
List<T> search(RepositoryMethodContext metadata, String text, Limit limit) {
Class<T> domainType = metadata.repository().getDomainType();
Class<T> domainType = metadata.getRepository().getDomainType();
String indexName = domainType.getSimpleName().toLowerCase();
List<String> jsonResult = service.search(indexName, text, 0, limit.max());
@@ -312,7 +312,8 @@ 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
To make use of the extension simply add the interface to the repository as shown below.
The infrastructure will take care placing the required `RepositoryMethodContext` so all that
====
[source,java]

View File

@@ -0,0 +1,75 @@
/*
* 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.core.NamedThreadLocal;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.lang.Nullable;
/**
* Class containing value objects providing information about the current repository method invocation.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
class DefaultRepositoryMethodContext implements RepositoryMethodContext {
/**
* ThreadLocal holder for repository method associated with this thread. Will contain {@code null} unless the
* "exposeMetadata" property on the controlling repository factory configuration has been set to "true".
*/
private static final ThreadLocal<RepositoryMethodContext> currentMethod = new NamedThreadLocal<>(
"Current Repository Method");
private final RepositoryMetadata repositoryMetadata;
private final Method method;
public DefaultRepositoryMethodContext(RepositoryMetadata repositoryMetadata, Method method) {
this.repositoryMetadata = repositoryMetadata;
this.method = method;
}
@Nullable
public static RepositoryMethodContext getMetadata() {
return currentMethod.get();
}
@Nullable
public static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) {
RepositoryMethodContext old = currentMethod.get();
if (metadata != null) {
currentMethod.set(metadata);
} else {
currentMethod.remove();
}
return old;
}
@Override
public RepositoryMetadata getRepository() {
return repositoryMetadata;
}
@Override
public Method getMethod() {
return method;
}
}

View File

@@ -1,79 +0,0 @@
/*
* 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 {
}
}

View File

@@ -33,7 +33,6 @@ 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;
@@ -164,9 +163,7 @@ class QueryExecutorMethodInterceptor implements MethodInterceptor {
RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);
if (invocationMetadata == null) {
DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(repositoryInformation, method);
invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(repositoryMethodMetadata, queries.get(method));
invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));
invocationMetadataCache.put(method, invocationMetadata);
}

View File

@@ -32,7 +32,6 @@ 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;
@@ -282,7 +281,7 @@ public class RepositoryComposition {
ReflectionUtils.makeAccessible(methodToCall);
return fragments.invoke(metadata, listener,
return fragments.invoke(metadata != null ? metadata.getRepositoryInterface() : method.getDeclaringClass(), listener,
method, methodToCall, argumentConverter.apply(methodToCall, args));
}
@@ -370,6 +369,7 @@ public class RepositoryComposition {
private final List<RepositoryFragment<?>> fragments;
private RepositoryFragments(List<RepositoryFragment<?>> fragments) {
this.fragments = fragments;
}
@@ -382,10 +382,6 @@ public class RepositoryComposition {
return EMPTY;
}
public static RepositoryFragments empty(RepositoryMetadata metadata) {
return EMPTY;
}
/**
* Create {@link RepositoryFragments} from just implementation objects.
*
@@ -488,7 +484,7 @@ public class RepositoryComposition {
/**
* Invoke {@link Method} by resolving the fragment that implements a suitable method.
*
* @param metadata
* @param repositoryInterface
* @param listener
* @param invokedMethod invoked method as per invocation on the interface.
* @param methodToCall backend method that is backing the call.
@@ -497,7 +493,7 @@ public class RepositoryComposition {
* @throws Throwable
*/
@Nullable
Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMulticaster listener, Method invokedMethod,
Object invoke(Class<?> repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod,
Method methodToCall, Object[] args) throws Throwable {
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
@@ -511,15 +507,12 @@ public class RepositoryComposition {
if (repositoryMethodInvoker == null) {
DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(metadata, invokedMethod, methodToCall);
repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(repositoryMethodMetadata, optional.get(),
repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(invokedMethod, optional.get(),
methodToCall);
invocationMetadataCache.put(invokedMethod, repositoryMethodInvoker);
}
Class<?> target = (metadata != null && metadata.getRepositoryInterface() != null) ? metadata.getRepositoryInterface() : invokedMethod.getDeclaringClass();
return repositoryMethodInvoker.invoke(target, listener, args);
return repositoryMethodInvoker.invoke(repositoryInterface, listener, args);
}
private RepositoryFragment<?> findImplementationFragment(Method key) {

View File

@@ -69,6 +69,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private final Class<? extends T> repositoryInterface;
private RepositoryFactorySupport factory;
private boolean exposeMetadata;
private Key queryLookupStrategyKey;
private Optional<Class<?>> repositoryBaseClass = Optional.empty();
private Optional<Object> customImplementation = Optional.empty();
@@ -107,6 +108,18 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass);
}
/**
* Set whether the repository method metadata should be exposed by the repository factory as a ThreadLocal for
* retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
* repository information.
* <p>
* Default is "false", in order to avoid unnecessary extra interception. This means that no guarantees are provided
* that {@code RepositoryMethodContext} access will work consistently within any method of the advised object.
*/
public void setExposeMetadata(boolean exposeMetadata) {
this.exposeMetadata = exposeMetadata;
}
/**
* Set the {@link QueryLookupStrategy.Key} to be used.
*
@@ -258,6 +271,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
public void afterPropertiesSet() {
this.factory = createRepositoryFactory();
this.factory.setExposeMetadata(exposeMetadata);
this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
this.factory.setNamedQueries(namedQueries);
this.factory.setEvaluationContextProvider(

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.repository.core.support;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -29,6 +30,7 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
@@ -94,6 +96,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
private final List<RepositoryProxyPostProcessor> postProcessors;
private Optional<Class<?>> repositoryBaseClass;
private boolean exposeMetadata;
private @Nullable QueryLookupStrategy.Key queryLookupStrategyKey;
private List<QueryCreationListener<?>> queryPostProcessors;
private List<RepositoryMethodInvocationListener> methodInvocationListeners;
@@ -121,6 +124,18 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
this.projectionFactory = createProjectionFactory();
}
/**
* Set whether the repository method metadata should be exposed by the repository factory as a ThreadLocal for
* retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
* repository information.
* <p>
* Default is "false", in order to avoid unnecessary extra interception. This means that no guarantees are provided
* that {@code RepositoryMethodContext} access will work consistently within any method of the advised object.
*/
public void setExposeMetadata(boolean exposeMetadata) {
this.exposeMetadata = exposeMetadata;
}
/**
* Sets the strategy of how to lookup a query to execute finders.
*
@@ -330,7 +345,10 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
result.addAdvice(new MethodInvocationValidator());
}
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
if (this.exposeMetadata) {
result.addAdvice(new ExposeMetadataInterceptor(metadata));
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
}
if (!postProcessors.isEmpty()) {
StartupStep repositoryPostprocessorsStep = onEvent(applicationStartup, "spring.data.repository.postprocessors",
@@ -634,6 +652,32 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
}
}
/**
* Interceptor for repository proxies when the repository needs exposing metadata.
*/
private static class ExposeMetadataInterceptor implements MethodInterceptor, Serializable {
private final RepositoryMetadata repositoryMetadata;
public ExposeMetadataInterceptor(RepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
}
@Nullable
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
RepositoryMethodContext oldMetadata = null;
try {
oldMetadata = RepositoryMethodContext
.setCurrentMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod()));
return invocation.proceed();
} finally {
RepositoryMethodContext.setCurrentMetadata(oldMetadata);
}
}
}
/**
* {@link QueryCreationListener} collecting the {@link QueryMethod}s created for all query methods of the repository
* interface.

View File

@@ -0,0 +1,93 @@
/*
* 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;
/**
* Interface containing methods and value objects to obtain information about the current repository method invocation.
* <p>
* The {@link #currentMethod()} method is usable if the repository factory is configured to expose the current
* repository method metadata (not the default). It returns the invoked repository method. Target objects or advice can
* use this to make advised calls.
* <p>
* Spring Data's framework does not expose method metadata by default, as there is a performance cost in doing so.
* <p>
* The functionality in this class might be used by a target object that needed access to resources on the invocation.
* However, this approach should not be used when there is a reasonable alternative, as it makes application code
* dependent on usage in particular.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
public interface RepositoryMethodContext {
/**
* Try to return the current repository method metadata. This method is usable only if the calling method has been
* invoked via a repository method, and the repository factory has been set to expose metadata. Otherwise, this method
* will throw an IllegalStateException.
*
* @return the current repository method metadata (never returns {@code null})
* @throws IllegalStateException if the repository method metadata cannot be found, because the method was invoked
* outside a repository method invocation context, or because the repository has not been configured to
* expose its metadata.
*/
static RepositoryMethodContext currentMethod() throws IllegalStateException {
RepositoryMethodContext metadata = DefaultRepositoryMethodContext.getMetadata();
if (metadata == null) {
throw new IllegalStateException(
"Cannot find current repository method: Set 'exposeMetadata' property on RepositoryFactorySupport to 'true' to make it available, and "
+ "ensure that RepositoryMethodContext.currentMethod() is invoked in the same thread as the repository invocation.");
}
return metadata;
}
/**
* Make the given repository method metadata available via the {@link #currentMethod()} method.
* <p>
* Note that the caller should be careful to keep the old value as appropriate.
*
* @param metadata the metadata to expose (or {@code null} to reset it)
* @return the old metadata, which may be {@code null} if none was bound
* @see #currentMethod()
*/
@Nullable
static RepositoryMethodContext setCurrentMetadata(@Nullable RepositoryMethodContext metadata) {
return DefaultRepositoryMethodContext.setMetadata(metadata);
}
/**
* Returns the metadata for the repository.
*
* @return the repository metadata.
*/
RepositoryMetadata getRepository();
/**
* Returns the current method that is being invoked.
* <p>
* The method object represents the method as being invoked on the repository interface. It doesn't match the backing
* repository implementation in case the method invocation is delegated to an implementation method.
*
* @return the current method.
*/
Method getMethod();
}

View File

@@ -18,19 +18,12 @@ 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;
@@ -53,8 +46,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
*/
@@ -64,13 +57,11 @@ abstract class RepositoryMethodInvoker {
private final Class<?> returnedType;
private final Invokable invokable;
private final boolean suspendedDeclaredMethod;
protected RepositoryMethodMetadata repositoryMethodMetadata;
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetadata, Invokable invokable) {
protected RepositoryMethodInvoker(Method method, Invokable invokable) {
this.repositoryMethodMetadata = repositoryMethodMetadata;
this.method = repositoryMethodMetadata.method().declaredMethod();
this.method = method;
if (KotlinDetector.isKotlinReflectPresent()) {
@@ -125,7 +116,7 @@ abstract class RepositoryMethodInvoker {
}
}
static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata declaredMethod, RepositoryQuery query) {
static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) {
return new RepositoryQueryMethodInvoker(declaredMethod, query);
}
@@ -137,7 +128,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(RepositoryMethodMetadata declaredMethod, Object instance, Method baseMethod) {
static RepositoryMethodInvoker forFragmentMethod(Method declaredMethod, Object instance, Method baseMethod) {
return new RepositoryFragmentMethodInvoker(declaredMethod, instance, baseMethod);
}
@@ -176,10 +167,6 @@ 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())) {
@@ -197,8 +184,6 @@ abstract class RepositoryMethodInvoker {
} catch (Exception e) {
multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e)));
throw e;
} finally {
DefaultRepositoryMethodMetadata.unbind();
}
}
@@ -217,7 +202,7 @@ abstract class RepositoryMethodInvoker {
* Implementation to invoke query methods.
*/
private static class RepositoryQueryMethodInvoker extends RepositoryMethodInvoker {
public RepositoryQueryMethodInvoker(RepositoryMethodMetadata method, RepositoryQuery repositoryQuery) {
public RepositoryQueryMethodInvoker(Method method, RepositoryQuery repositoryQuery) {
super(method, repositoryQuery::execute);
}
}
@@ -270,19 +255,15 @@ abstract class RepositoryMethodInvoker {
*/
private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker {
public RepositoryFragmentMethodInvoker(RepositoryMethodMetadata metadata, Object instance, Method baseClassMethod) {
this(CoroutineAdapterInformation.create(metadata.method().declaredMethod(), baseClassMethod), metadata, instance,
public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) {
this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance,
baseClassMethod);
}
public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, RepositoryMethodMetadata declaredMethod,
public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, Method 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.

View File

@@ -1,44 +0,0 @@
/*
* 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();
}
}

View File

@@ -15,12 +15,8 @@
*/
package org.springframework.data.repository.core.support;
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 static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -31,11 +27,8 @@ 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;
/**
@@ -170,33 +163,6 @@ 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<Person, String>, QueryByExampleExecutor<Person> {
Person save(Person entity);
@@ -236,10 +202,6 @@ class RepositoryCompositionUnitTests {
}
interface CapturingRepository extends Repository<Person, String>, FooMixin {
}
interface FooMixin {
String getString();
@@ -254,23 +216,6 @@ class RepositoryCompositionUnitTests {
}
}
class MethodMetadataCapturingMixin implements FooMixin {
List<RepositoryMethodMetadata> 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();

View File

@@ -30,6 +30,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -40,6 +41,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
@@ -117,7 +119,7 @@ class RepositoryFactorySupportUnitTests {
factory.getRepository(ObjectRepository.class);
verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class));
verify(otherListener, times(2)).onCreation(any(RepositoryQuery.class));
verify(otherListener, times(3)).onCreation(any(RepositoryQuery.class));
}
@Test // DATACMNS-1538
@@ -154,8 +156,7 @@ class RepositoryFactorySupportUnitTests {
@Test // DATACMNS-102
void invokesCustomMethodCompositionMethodIfItRedeclaresACRUDOne() {
var repository = factory.getRepository(ObjectRepository.class,
RepositoryFragments.just(customImplementation));
var repository = factory.getRepository(ObjectRepository.class, RepositoryFragments.just(customImplementation));
repository.findById(1);
verify(customImplementation, times(1)).findById(1);
@@ -247,6 +248,29 @@ class RepositoryFactorySupportUnitTests {
assertThat(repositoryMethodInvocation.getResult().getError()).isInstanceOf(IllegalStateException.class);
}
@Test // GH-3090
void capturesRepositoryMetadata() {
record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {
}
when(factory.queryOne.execute(any(Object[].class)))
.then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(),
ExposeInvocationInterceptor.currentInvocation()));
factory.setExposeMetadata(true);
var repository = factory.getRepository(ObjectRepository.class);
var metadataByLastname = repository.findMetadataByLastname();
assertThat(metadataByLastname).isInstanceOf(Metadata.class);
Metadata metadata = (Metadata) metadataByLastname;
assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname");
assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class);
assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
}
@Test // DATACMNS-509, DATACMNS-1764
void convertsWithSameElementType() {
@@ -283,8 +307,8 @@ class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> factory.addRepositoryProxyPostProcessor(null)) //
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining(RepositoryProxyPostProcessor.class.getSimpleName());
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining(RepositoryProxyPostProcessor.class.getSimpleName());
}
@Test // DATACMNS-715, SPR-13109
@@ -334,9 +358,9 @@ class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> factory.getTargetRepositoryViaReflection(information, entityInformation, "Foo")) //
.isInstanceOf(IllegalStateException.class) //
.hasMessageContaining(entityInformation.getClass().getName()) //
.hasMessageContaining(String.class.getName());
.isInstanceOf(IllegalStateException.class) //
.hasMessageContaining(entityInformation.getClass().getName()) //
.hasMessageContaining(String.class.getName());
}
@Test
@@ -357,8 +381,8 @@ class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> repository.findById("")) //
.isInstanceOf(EmptyResultDataAccessException.class) //
.hasMessageContaining("Result must not be null");
.isInstanceOf(EmptyResultDataAccessException.class) //
.hasMessageContaining("Result must not be null");
assertThat(repository.findByUsername("")).isNull();
}
@@ -370,8 +394,8 @@ class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> repository.findByClass(null)) //
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining("must not be null");
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining("must not be null");
}
@Test // DATACMNS-1154
@@ -391,8 +415,8 @@ class RepositoryFactorySupportUnitTests {
assertThatThrownBy( //
() -> repository.findById(null)) //
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining("must not be null"); //
.isInstanceOf(IllegalArgumentException.class) //
.hasMessageContaining("must not be null"); //
}
@Test // DATACMNS-1154
@@ -509,6 +533,8 @@ class RepositoryFactorySupportUnitTests {
@Nullable
Object save(Object entity);
Object findMetadataByLastname();
static String staticMethod() {
return "OK";
}

View File

@@ -15,11 +15,8 @@
*/
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 static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import kotlin.coroutines.Continuation;
import kotlinx.coroutines.reactive.ReactiveFlowKt;
@@ -49,12 +46,11 @@ 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 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;
@@ -318,8 +314,7 @@ class RepositoryMethodInvokerUnitTests {
RepositoryMethodInvokerStub(Class<?> repositoryInterface, RepositoryInvocationMulticaster multicaster,
String methodName, Invokable invokable) {
super(DefaultRepositoryMethodMetadata.repositoryMethodMetadata(mock(RepositoryMetadata.class), methodByName(repositoryInterface, methodName)), invokable);
super(methodByName(repositoryInterface, methodName), invokable);
this.repositoryInterface = repositoryInterface;
this.multicaster = multicaster;
}