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:
committed by
Christoph Strobl
parent
647d9fd949
commit
658c04d00e
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user