Move RepositoryMethodContext to repository.core package.

RepositoryMethodContext are now made available for dependency injection via RepositoryConfigurationExtensionSupport.registerBeansForRoot(…). Moved RMC into repository.core package (previously repository.core.support) and only expose factory methods on DefaultRepositoryMethodContext. DRMC also exposes a injection proxy lookup method that creates a proxy equipped with a TargetSource delegating to DRMC.getInstance() (previously ….getContext()). An additional, static DRMC.forMethod(…) allows the creation of a default instance for testing purposes.

Rename getRepository() to getMetadata() on RMC.

Fixes #3175.
Original pull request: #3176
This commit is contained in:
Oliver Drotbohm
2024-10-14 16:59:22 +02:00
committed by Mark Paluch
parent 55afe84b44
commit 323aa60808
8 changed files with 207 additions and 108 deletions

View File

@@ -31,12 +31,15 @@ import org.springframework.beans.factory.config.BeanDefinition;
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 abstract class RepositoryConfigurationExtensionSupport implements Reposit
@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.

View File

@@ -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.
* <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
* @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.
* <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, will never be {@literal null}..
*/
Method getMethod();
}

View File

@@ -17,18 +17,22 @@ package org.springframework.data.repository.core.support;
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 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext {
static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) {
RepositoryMethodContext old = currentMethod.get();
if (metadata != null) {
currentMethod.set(metadata);
} else {
@@ -65,7 +95,7 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext {
}
@Override
public RepositoryMetadata getRepository() {
public RepositoryMetadata getMetadata() {
return repositoryMetadata;
}
@@ -73,5 +103,4 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext {
public Method getMethod() {
return method;
}
}

View File

@@ -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<T> implements TargetSource {
private final Class<T> type;
private final Supplier<? extends T> 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<T> type, Supplier<? extends T> 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;
}
}

View File

@@ -56,6 +56,7 @@ import org.springframework.data.repository.core.EntityInformation;
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;
@@ -772,15 +773,18 @@ public abstract class RepositoryFactorySupport
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);
}
}
}
/**

View File

@@ -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.
* <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
* @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.
* <p>
* Note that the caller should be careful to keep the old value as appropriate.
*
* @param metadata the metadata to expose (or {@code null} to reset it)
* @return the old metadata, which may be {@code null} if none was bound
* @see #currentMethod()
*/
@Nullable
static RepositoryMethodContext setCurrentMetadata(@Nullable RepositoryMethodContext metadata) {
return DefaultRepositoryMethodContext.setMetadata(metadata);
}
/**
* Returns the metadata for the repository.
*
* @return the repository metadata.
*/
RepositoryMetadata getRepository();
/**
* Returns the current method that is being invoked.
* <p>
* The method object represents the method as being invoked on the repository interface. It doesn't match the backing
* repository implementation in case the method invocation is delegated to an implementation method.
*
* @return the current method.
*/
Method getMethod();
}

View File

@@ -28,7 +28,6 @@ import org.mockito.Mockito;
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 @@ class RepositoryConfigurationDelegateUnitTests {
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);

View File

@@ -251,11 +251,10 @@ class RepositoryFactorySupportUnitTests {
@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 @@ class RepositoryFactorySupportUnitTests {
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");
}