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 42f4de5cde..ecd405442b 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; @@ -109,7 +112,13 @@ public String getDefaultNamedQueryLocation() { @Override public void registerBeansForRoot(BeanDefinitionRegistry registry, - RepositoryConfigurationSource configurationSource) {} + RepositoryConfigurationSource configurationSource) { + + // A proxy RepositoryMethodContext for dependency injection + registerIfNotAlreadyRegistered( + () -> new RootBeanDefinition(RepositoryMethodContext.class, DefaultRepositoryMethodContext::getInjectionProxy), + registry, "repositoryMethodContextFactory", configurationSource); + } /** * 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
- * 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 529671b228..a2be6d322e 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 2661998cb2..6823989c94 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");
}