Skip to content

Commit

Permalink
Move responsibility for storing method context to dedicated component.
Browse files Browse the repository at this point in the history
See #3175.
Original pull request: #3176
  • Loading branch information
christophstrobl authored and mp911de committed Oct 16, 2024
1 parent 323aa60 commit 24c31bf
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
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;
Expand Down Expand Up @@ -99,5 +102,13 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader)
// 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,15 +32,12 @@
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;
Expand Down Expand Up @@ -112,13 +110,7 @@ public String getDefaultNamedQueryLocation() {

@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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
/**
* 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>
Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@

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;

/**
Expand All @@ -34,13 +31,6 @@
*/
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;

Expand All @@ -64,36 +54,6 @@ public static RepositoryMethodContext forMethod(Method method) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
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;
Expand Down Expand Up @@ -776,13 +777,13 @@ public Object invoke(MethodInvocation invocation) throws Throwable {

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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,24 +276,6 @@ 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
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;
Expand Down Expand Up @@ -254,7 +256,7 @@ void capturesRepositoryMetadata() {
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);
Expand All @@ -277,7 +279,7 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati
}

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() {});
Expand All @@ -287,7 +289,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");
}

Expand Down

0 comments on commit 24c31bf

Please sign in to comment.