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:
committed by
Mark Paluch
parent
55afe84b44
commit
323aa60808
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user