diff --git a/pom.xml b/pom.xml index c6dc5a59a0..a5d61121ba 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.4.0-SNAPSHOT + 3.4.0-GH-3175-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc index 1d85f90e47..d279a359d8 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -271,7 +271,7 @@ package com.acme.search; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; -import org.springframework.data.repository.core.support.RepositoryMethodContext; +import org.springframework.data.repository.core.RepositoryMethodContext; class DefaultSearchExtension implements SearchExtension { @@ -282,7 +282,7 @@ class DefaultSearchExtension implements SearchExtension { } public List search(String text, Limit limit) { - return search(RepositoryMethodContext.currentMethod(), text, limit); + return search(RepositoryMethodContext.getContext(), text, limit); } List search(RepositoryMethodContext metadata, String text, Limit limit) { @@ -297,12 +297,12 @@ class DefaultSearchExtension implements SearchExtension { } ---- -In the example above `RepositoryMethodContext.currentMethod()` is used to retrieve metadata for the actual method invocation. +In the example above `RepositoryMethodContext.getContext()` is used to retrieve metadata for the actual method invocation. `RepositoryMethodContext` exposes information attached to the repository such as the domain type. In this case we use the repository domain type to identify the name of the index to be searched. Exposing invocation metadata is costly, hence it is disabled by default. -To access `RepositoryMethodContext.currentMethod()` you need to advise the repository factory responsible for creating the actual repository to expose method metadata. +To access `RepositoryMethodContext.getContext()` you need to advise the repository factory responsible for creating the actual repository to expose method metadata. .Expose Repository Metadata [tabs] @@ -319,7 +319,7 @@ package com.acme.search; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.repository.core.support.RepositoryMetadataAccess; -import org.springframework.data.repository.core.support.RepositoryMethodContext; +import org.springframework.data.repository.core.RepositoryMethodContext; class DefaultSearchExtension implements SearchExtension, RepositoryMetadataAccess { diff --git a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java index 5fafd13a1e..7658d9128f 100644 --- a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java +++ b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java @@ -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; @@ -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)); } } 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..7efe44c9c1 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -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; diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java similarity index 58% rename from src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java rename to src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java index 516c8d95b6..bef865c893 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContext.java @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.core.support; +package org.springframework.data.repository.core; 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. *

- * 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. *

* Spring Data's framework does not expose method metadata by default, as there is a performance cost in doing so. *

@@ -35,7 +32,8 @@ * * @author Christoph Strobl * @author Mark Paluch - * @since 3.4.0 + * @author Oliver Drotbohm + * @since 3.4 */ public interface RepositoryMethodContext { @@ -49,37 +47,16 @@ public interface RepositoryMethodContext { * 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); + static RepositoryMethodContext getContext() throws IllegalStateException { + return RepositoryMethodContextHolder.getContext(); } /** * Returns the metadata for the repository. * - * @return the repository metadata. + * @return the repository metadata, will never be {@literal null}. */ - RepositoryMetadata getRepository(); + RepositoryMetadata getMetadata(); /** * Returns the current method that is being invoked. @@ -87,8 +64,7 @@ static RepositoryMethodContext setCurrentMetadata(@Nullable RepositoryMethodCont * 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. + * @return the current method, will never be {@literal null}. */ Method getMethod(); - } diff --git a/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java new file mode 100644 index 0000000000..bc57f152f7 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/RepositoryMethodContextHolder.java @@ -0,0 +1,86 @@ +/* + * 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; + +/** + * Associates a given {@link RepositoryMethodContext} with the current execution thread. + *

+ * This class provides a series of static methods that interact with a thread-local storage of + * {@link RepositoryMethodContext}. The purpose of the class is to provide a convenient way to be used for an + * application. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.4 + * @see RepositoryMethodContext + */ +public class RepositoryMethodContextHolder { + + /** + * 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 {@code true}. + */ + private static final ThreadLocal currentMethod = new NamedThreadLocal<>( + "Current Repository Method"); + + /** + * Make the given repository method metadata available via the {@link #getContext()} method. + *

+ * Note that the caller should be careful to keep the old value as appropriate. + * + * @param context the metadata to expose (or {@code null} to reset it) + * @return the old metadata, which may be {@code null} if none was bound + * @see #getContext() + */ + @Nullable + public static RepositoryMethodContext setContext(@Nullable RepositoryMethodContext context) { + + RepositoryMethodContext old = currentMethod.get(); + if (context != null) { + currentMethod.set(context); + } else { + currentMethod.remove(); + } + + return old; + } + + /** + * 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. + */ + public static RepositoryMethodContext getContext() { + + RepositoryMethodContext metadata = currentMethod.get(); + + 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.getContext() is invoked in the same thread as the repository invocation."); + } + + return metadata; + } +} 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..13e14f6ea1 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,25 +17,19 @@ import java.lang.reflect.Method; -import org.springframework.core.NamedThreadLocal; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.core.RepositoryMethodContext; +import org.springframework.util.Assert; /** * Class containing value objects providing information about the current repository method invocation. * * @author Christoph Strobl * @author Mark Paluch - * @since 3.4.0 + * @author Oliver Drotbohm + * @since 3.4 */ -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 currentMethod = new NamedThreadLocal<>( - "Current Repository Method"); +public class DefaultRepositoryMethodContext implements RepositoryMethodContext { private final RepositoryMetadata repositoryMetadata; private final Method method; @@ -46,26 +40,22 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext { this.method = method; } - @Nullable - static RepositoryMethodContext getMetadata() { - return currentMethod.get(); - } - - @Nullable - static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) { + /** + * 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) { - RepositoryMethodContext old = currentMethod.get(); - if (metadata != null) { - currentMethod.set(metadata); - } else { - currentMethod.remove(); - } + Assert.notNull(method, "Method must not be null!"); - return old; + return new DefaultRepositoryMethodContext(AbstractRepositoryMetadata.getMetadata(method.getDeclaringClass()), + method); } @Override - public RepositoryMetadata getRepository() { + public RepositoryMetadata getMetadata() { return repositoryMetadata; } @@ -73,5 +63,4 @@ public RepositoryMetadata getRepository() { public Method getMethod() { return method; } - } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index 967d02d158..3059bbf209 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -121,7 +121,7 @@ public void setRepositoryBaseClass(Class repositoryBaseClass) { * Default is "false", in order to avoid unnecessary extra interception. This means that no guarantees are provided * that {@code RepositoryMethodContext} access will work consistently within any method of the advised object. * - * @since 3.4.0 + * @since 3.4 */ public void setExposeMetadata(boolean exposeMetadata) { this.exposeMetadata = exposeMetadata; diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 58b47ad62d..8730942c99 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -56,6 +56,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.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; @@ -772,15 +774,18 @@ public ExposeMetadataInterceptor(RepositoryMetadata repositoryMetadata) { public Object invoke(MethodInvocation invocation) throws Throwable { RepositoryMethodContext oldMetadata = null; + try { - oldMetadata = RepositoryMethodContext - .setCurrentMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod())); + + oldMetadata = RepositoryMethodContextHolder + .setContext(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod())); + return invocation.proceed(); + } finally { - RepositoryMethodContext.setCurrentMetadata(oldMetadata); + RepositoryMethodContextHolder.setContext(oldMetadata); } } - } /** diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java index fb4a7b82ce..4122e20617 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java @@ -27,7 +27,7 @@ * * @author Mark Paluch * @since 3.4 - * @see RepositoryMethodContext + * @see org.springframework.data.repository.core.RepositoryMethodContext */ public interface RepositoryMetadataAccess { 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..ebdaddef42 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; 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..f6371c1506 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 @@ -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; @@ -251,11 +253,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(RepositoryMethodContextHolder.getContext(), ExposeInvocationInterceptor.currentInvocation())); factory.setExposeMetadata(true); @@ -267,7 +268,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"); } @@ -278,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.getContext(), ExposeInvocationInterceptor.currentInvocation())); var repository = factory.getRepository(ObjectRepository.class, new RepositoryMetadataAccess() {}); @@ -288,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"); }