diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java index 6fc0cf4855..0d3c36fc07 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -31,12 +31,15 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.ResourceLoader; import org.springframework.core.log.LogMessage; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.RepositoryMethodContext; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMethodContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -106,7 +109,12 @@ public String getDefaultNamedQueryLocation() { } public void registerBeansForRoot(BeanDefinitionRegistry registry, - RepositoryConfigurationSource configurationSource) {} + RepositoryConfigurationSource configurationSource) { + + // A proxy RepositoryMethodContext for dependency injection + registry.registerBeanDefinition("repositoryMethodContextFactory", + new RootBeanDefinition(RepositoryMethodContext.class, DefaultRepositoryMethodContext::getInjectionProxy)); + } /** * Returns the prefix of the module to be used to create the default location for Spring Data named queries. diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java new file mode 100644 index 0000000000..da4b4da3de --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java @@ -0,0 +1,56 @@ +/* + * 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; + +import java.lang.reflect.Method; + +/** + * 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 + * @author Oliver Drotbohm + * @since 3.4.0 + */ +public interface RepositoryMethodContext { + + /** + * Returns the metadata for the repository. + * + * @return the repository metadata, will never be {@literal null}. + */ + RepositoryMetadata getMetadata(); + + /** + * 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, will never be {@literal null}.. + */ + Method getMethod(); +} 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 index 8f87c16785..cf47441a71 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java @@ -17,18 +17,22 @@ import java.lang.reflect.Method; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.NamedThreadLocal; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.RepositoryMethodContext; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Class containing value objects providing information about the current repository method invocation. * * @author Christoph Strobl * @author Mark Paluch + * @author Oliver Drotbohm * @since 3.4.0 */ -class DefaultRepositoryMethodContext implements RepositoryMethodContext { +public class DefaultRepositoryMethodContext implements RepositoryMethodContext { /** * ThreadLocal holder for repository method associated with this thread. Will contain {@code null} unless the @@ -46,8 +50,33 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext { this.method = method; } + /** + * Creates a new {@link RepositoryMethodContext} for the given {@link Method}. + * + * @param method must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static RepositoryMethodContext forMethod(Method method) { + + Assert.notNull(method, "Method must not be null!"); + + return new DefaultRepositoryMethodContext(AbstractRepositoryMetadata.getMetadata(method.getDeclaringClass()), + method); + } + + /** + * Creates a proxy {@link RepositoryMethodContext} instance suitable for dependency injection. + * + * @return will never be {@literal null}. + */ + public static RepositoryMethodContext getInjectionProxy() { + + return ProxyFactory.getProxy(RepositoryMethodContext.class, + new DynamicLookupTargetSource<>(RepositoryMethodContext.class, () -> getInstance())); + } + @Nullable - static RepositoryMethodContext getMetadata() { + static RepositoryMethodContext getInstance() { return currentMethod.get(); } @@ -55,6 +84,7 @@ static RepositoryMethodContext getMetadata() { static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) { RepositoryMethodContext old = currentMethod.get(); + if (metadata != null) { currentMethod.set(metadata); } else { @@ -65,7 +95,7 @@ static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext met } @Override - public RepositoryMetadata getRepository() { + public RepositoryMetadata getMetadata() { return repositoryMetadata; } @@ -73,5 +103,4 @@ public RepositoryMetadata getRepository() { public Method getMethod() { return method; } - } diff --git a/src/main/java/org/springframework/data/repository/core/support/DynamicLookupTargetSource.java b/src/main/java/org/springframework/data/repository/core/support/DynamicLookupTargetSource.java new file mode 100644 index 0000000000..0a7023014f --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/DynamicLookupTargetSource.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support; + +import java.util.function.Supplier; + +import org.springframework.aop.TargetSource; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link TargetSource}, that will re-obtain an instance using the configured supplier. + * + * @author Oliver Drotbohm + * @since 3.4.0 + */ +class DynamicLookupTargetSource implements TargetSource { + + private final Class type; + private final Supplier supplier; + + /** + * Creates a new {@link DynamicLookupTargetSource} for the given type and {@link Supplier}. + * + * @param type must not be {@literal null}. + * @param supplier must not be {@literal null}. + */ + public DynamicLookupTargetSource(Class type, Supplier supplier) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(supplier, "Supplier must not be null!"); + + this.type = type; + this.supplier = supplier; + } + + /* + * (non-Javadoc) + * @see org.springframework.aop.TargetSource#isStatic() + */ + @Override + public boolean isStatic() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.aop.TargetSource#getTarget() + */ + @Override + @Nullable + public Object getTarget() throws Exception { + return supplier.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.aop.TargetSource#getTargetClass() + */ + @Override + @NonNull + public Class getTargetClass() { + return type; + } +} 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 88965a4abe..216b31814e 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 @@ -51,6 +51,7 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.RepositoryMethodContext; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; @@ -668,15 +669,18 @@ public ExposeMetadataInterceptor(RepositoryMetadata repositoryMetadata) { public Object invoke(MethodInvocation invocation) throws Throwable { RepositoryMethodContext oldMetadata = null; + try { - oldMetadata = RepositoryMethodContext - .setCurrentMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod())); + + oldMetadata = DefaultRepositoryMethodContext + .setMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod())); + return invocation.proceed(); + } finally { - RepositoryMethodContext.setCurrentMetadata(oldMetadata); + DefaultRepositoryMethodContext.setMetadata(oldMetadata); } } - } /** 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 deleted file mode 100644 index 516c8d95b6..0000000000 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java +++ /dev/null @@ -1,94 +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; - -/** - * 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 - * @since 3.4.0 - */ -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/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java index 6ba7582203..07b0f7c0c4 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java @@ -28,7 +28,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.aop.framework.Advised; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.ListableBeanFactory; @@ -277,6 +276,24 @@ void considersGenericLength() { assertThat(it.getGeneric(1).resolve()).isEqualTo(Person.class); } + @Test // GH-3175 + void registersRepositoryMethodContextForInjection() { + + var environment = new StandardEnvironment(); + var context = new GenericApplicationContext(); + context.registerBean("fragment", MyFragmentImpl.class); + + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + AnnotationMetadata.introspect(TestConfig.class), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory(), new AnnotationBeanNameGenerator()); + + var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); + + delegate.registerRepositoriesIn(context, extension); + + assertThat(context.containsBeanDefinition("repositoryMethodContextFactory")).isTrue(); + } + private static ListableBeanFactory assertLazyRepositoryBeanSetup(Class configClass) { var context = new AnnotationConfigApplicationContext(configClass); 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 79f2862bf9..b28bb656a2 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 @@ -251,11 +251,10 @@ void capturesFailureFromInvocation() { @Test // GH-3090 void capturesRepositoryMetadata() { - record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) { - } + record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {} when(factory.queryOne.execute(any(Object[].class))) - .then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(), + .then(invocation -> new Metadata(DefaultRepositoryMethodContext.getInstance(), ExposeInvocationInterceptor.currentInvocation())); factory.setExposeMetadata(true); @@ -267,7 +266,7 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati Metadata metadata = (Metadata) metadataByLastname; assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname"); - assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class); + assertThat(metadata.context().getMetadata().getDomainType()).isEqualTo(Object.class); assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname"); }