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 8aa643643..94048b432 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -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 implements SearchExtension { @@ -280,12 +280,12 @@ class DefaultSearchExtension implements SearchExtension { } public List search(String text, Limit limit) { - return search(RepositoryMethodMetadata.get(), text, limit); + return search(RepositoryMethodContext.currentMethod(), text, limit); } - List search(RepositoryMethodMetadata metadata, String text, Limit limit) { + List search(RepositoryMethodContext metadata, String text, Limit limit) { - Class domainType = metadata.repository().getDomainType(); + Class domainType = metadata.getRepository().getDomainType(); String indexName = domainType.getSimpleName().toLowerCase(); List 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] diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java new file mode 100644 index 000000000..5d1ed1e27 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.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 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; + } + +} 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 deleted file mode 100644 index 3a5b35f9a..000000000 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java +++ /dev/null @@ -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 { - } - -} 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 135b0d278..13309fa6a 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,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); } 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 2e01b208f..d26dedf25 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,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> fragments; private RepositoryFragments(List> 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) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index f3e6c13da..950b3cba9 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -69,6 +69,7 @@ public abstract class RepositoryFactoryBeanSupport, private final Class repositoryInterface; private RepositoryFactorySupport factory; + private boolean exposeMetadata; private Key queryLookupStrategyKey; private Optional> repositoryBaseClass = Optional.empty(); private Optional customImplementation = Optional.empty(); @@ -107,6 +108,18 @@ public abstract class RepositoryFactoryBeanSupport, 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. + *

+ * 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, public void afterPropertiesSet() { this.factory = createRepositoryFactory(); + this.factory.setExposeMetadata(exposeMetadata); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); this.factory.setEvaluationContextProvider( diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index b3babf465..4a026beae 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -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 postProcessors; private Optional> repositoryBaseClass; + private boolean exposeMetadata; private @Nullable QueryLookupStrategy.Key queryLookupStrategyKey; private List> queryPostProcessors; private List 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. + *

+ * 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. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java new file mode 100644 index 000000000..4c28b0ef8 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java @@ -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. + *

+ * 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. + *

+ * Spring Data's framework does not expose method metadata by default, as there is a performance cost in doing so. + *

+ * 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. + *

+ * 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. + *

+ * 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(); + +} 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 e4ba94a01..8647ba458 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,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. 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 deleted file mode 100644 index 6dd0cf178..000000000 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java +++ /dev/null @@ -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(); - } - -} 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 30322ef06..3181ed27b 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,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, QueryByExampleExecutor { Person save(Person entity); @@ -236,10 +202,6 @@ class RepositoryCompositionUnitTests { } - interface CapturingRepository extends Repository, FooMixin { - - } - interface FooMixin { String getString(); @@ -254,23 +216,6 @@ 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/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 71a470282..79f2862bf 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -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"; } 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 e1d0d9ea4..c1e68a4bd 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,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; }