Move responsibility for storing method context to dedicated component.

See #3175.
Original pull request: #3176
This commit is contained in:
Christoph Strobl
2024-10-15 14:45:10 +02:00
committed by Mark Paluch
parent 323aa60808
commit 24c31bfd65
8 changed files with 119 additions and 79 deletions

View File

@@ -18,11 +18,14 @@ package org.springframework.data.repository.aot.hint;
import java.util.Arrays;
import java.util.Properties;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.framework.Advised;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.io.InputStreamSource;
import org.springframework.data.domain.Example;
import org.springframework.data.mapping.context.MappingContext;
@@ -99,5 +102,13 @@ class RepositoryRuntimeHints implements RuntimeHintsRegistrar {
// annotated queries
hints.proxies().registerJdkProxy( //
TypeReference.of("org.springframework.data.annotation.QueryAnnotation"));
registerSpringProxy(TypeReference.of("org.springframework.data.repository.core.RepositoryMethodContext"), hints);
}
private static void registerSpringProxy(TypeReference type, RuntimeHints runtimeHints) {
runtimeHints.proxies().registerJdkProxy(type, TypeReference.of(SpringProxy.class),
TypeReference.of(Advised.class), TypeReference.of(DecoratingProxy.class));
}
}

View File

@@ -15,7 +15,8 @@
*/
package org.springframework.data.repository.config;
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*;
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.GENERATED_BEAN_NAME_SEPARATOR;
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.generateBeanName;
import java.lang.annotation.Annotation;
import java.util.Collection;
@@ -31,15 +32,12 @@ 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;
@@ -112,13 +110,7 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit
@Override
public void registerBeansForRoot(BeanDefinitionRegistry registry,
RepositoryConfigurationSource configurationSource) {
// A proxy RepositoryMethodContext for dependency injection
registerIfNotAlreadyRegistered(
() -> new RootBeanDefinition(RepositoryMethodContext.class, DefaultRepositoryMethodContext::getInjectionProxy),
registry, "repositoryMethodContextFactory", configurationSource);
}
RepositoryConfigurationSource configurationSource) {}
/**
* Returns the prefix of the module to be used to create the default location for Spring Data named queries.

View File

@@ -20,9 +20,9 @@ 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.
* The {@link #getMetadata()} 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>
@@ -50,7 +50,7 @@ public interface RepositoryMethodContext {
* 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}..
* @return the current method, will never be {@literal null}.
*/
Method getMethod();
}

View File

@@ -0,0 +1,92 @@
/*
* 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 org.springframework.core.NamedThreadLocal;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 3.4.0
*/
public class RepositoryMethodContextHolder {
private static ContextProvider contextSupplier;
static {
contextSupplier = new ThreadLocalContextProvider();
}
@Nullable
public static RepositoryMethodContext setContext(@Nullable RepositoryMethodContext context) {
return contextSupplier.set(context);
}
@Nullable
public static RepositoryMethodContext current() {
return contextSupplier.get();
}
public static void clearContext() {
contextSupplier.clear();
}
interface ContextProvider {
@Nullable
RepositoryMethodContext get();
@Nullable
RepositoryMethodContext set(@Nullable RepositoryMethodContext context);
void clear();
}
static class ThreadLocalContextProvider implements ContextProvider {
/**
* 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<RepositoryMethodContext> currentMethod = new NamedThreadLocal<>(
"Current Repository Method");
@Override
@Nullable
public RepositoryMethodContext get() {
return currentMethod.get();
}
public void clear() {
currentMethod.remove();
}
@Override
@Nullable
public RepositoryMethodContext set(@Nullable RepositoryMethodContext context) {
RepositoryMethodContext old = currentMethod.get();
if (context != null) {
currentMethod.set(context);
} else {
currentMethod.remove();
}
return old;
}
}
}

View File

@@ -17,11 +17,8 @@ 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;
/**
@@ -34,13 +31,6 @@ import org.springframework.util.Assert;
*/
public 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<RepositoryMethodContext> currentMethod = new NamedThreadLocal<>(
"Current Repository Method");
private final RepositoryMetadata repositoryMetadata;
private final Method method;
@@ -64,36 +54,6 @@ public class DefaultRepositoryMethodContext implements RepositoryMethodContext {
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 getInstance() {
return currentMethod.get();
}
@Nullable
static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) {
RepositoryMethodContext old = currentMethod.get();
if (metadata != null) {
currentMethod.set(metadata);
} else {
currentMethod.remove();
}
return old;
}
@Override
public RepositoryMetadata getMetadata() {
return repositoryMetadata;

View File

@@ -57,6 +57,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.RepositoryMethodContextHolder;
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;
@@ -776,13 +777,13 @@ public abstract class RepositoryFactorySupport
try {
oldMetadata = DefaultRepositoryMethodContext
.setMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod()));
oldMetadata = RepositoryMethodContextHolder
.setContext(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod()));
return invocation.proceed();
} finally {
DefaultRepositoryMethodContext.setMetadata(oldMetadata);
RepositoryMethodContextHolder.setContext(oldMetadata);
}
}
}

View File

@@ -276,24 +276,6 @@ 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

@@ -57,6 +57,8 @@ 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.RepositoryMethodContextHolder;
import org.springframework.data.repository.core.support.DummyRepositoryFactory.MyRepositoryQuery;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation;
@@ -254,7 +256,7 @@ class RepositoryFactorySupportUnitTests {
record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {}
when(factory.queryOne.execute(any(Object[].class)))
.then(invocation -> new Metadata(DefaultRepositoryMethodContext.getInstance(),
.then(invocation -> new Metadata(RepositoryMethodContextHolder.current(),
ExposeInvocationInterceptor.currentInvocation()));
factory.setExposeMetadata(true);
@@ -277,7 +279,7 @@ class RepositoryFactorySupportUnitTests {
}
when(factory.queryOne.execute(any(Object[].class)))
.then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(),
.then(invocation -> new Metadata(RepositoryMethodContextHolder.current(),
ExposeInvocationInterceptor.currentInvocation()));
var repository = factory.getRepository(ObjectRepository.class, new RepositoryMetadataAccess() {});
@@ -287,7 +289,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");
}