From e0bb1d43619ef276b71b976b01024f4790394be3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 May 2024 08:53:50 +0200 Subject: [PATCH 1/8] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c6dc5a59a0..1a885261d6 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-3090-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From a5ed24b04ab1d66f8c7a106c3219cf24c939a68c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 May 2024 10:50:44 +0200 Subject: [PATCH 2/8] Polishing. --- .../data/repository/config/FragmentMetadata.java | 2 -- .../data/repository/config/RepositoryWithFragmentExclusion.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java index 9136229445..c8ca604e0b 100644 --- a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java @@ -60,7 +60,6 @@ public Stream getFragmentInterfaces(String interfaceName) { * Returns whether the given interface is a fragment candidate. * * @param interfaceName must not be {@literal null} or empty. - * @param factory must not be {@literal null}. * @return */ private boolean isCandidate(String interfaceName) { @@ -69,7 +68,6 @@ private boolean isCandidate(String interfaceName) { AnnotationMetadata metadata = getAnnotationMetadata(interfaceName); return !metadata.hasAnnotation(NoRepositoryBean.class.getName()); - } private AnnotationMetadata getAnnotationMetadata(String className) { diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryWithFragmentExclusion.java b/src/test/java/org/springframework/data/repository/config/RepositoryWithFragmentExclusion.java index cdd314e976..3639ca6c53 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryWithFragmentExclusion.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryWithFragmentExclusion.java @@ -18,7 +18,7 @@ import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSourceUnitTests.Person; /** - * Repository with customized base base interface. + * Repository with customized base interface. * * @author Mark Paluch */ From 291fbcdbc9322203e1c31a37b8da7f2aa32b42e7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 May 2024 10:52:15 +0200 Subject: [PATCH 3/8] Consider fragments and repository contributions through spring.factories. --- .../RepositoryBeanDefinitionBuilder.java | 115 ++++++++++++++++-- .../RepositoryFragmentConfiguration.java | 25 ++-- ...anDefinitionRegistrarSupportUnitTests.java | 8 +- .../basepackage/repo/PersonRepository.java | 3 +- .../config/spifragment/SpiContribution.java | 23 ++++ .../config/spifragment/SpiFragment.java | 21 ++++ .../config/spifragment/SpiFragmentImpl.java | 23 ++++ src/test/resources/META-INF/spring.factories | 2 + 8 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java create mode 100644 src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java create mode 100644 src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index b9c70f087b..c23f54fc0b 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -19,14 +19,18 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -35,14 +39,18 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.data.config.ParsingUtils; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; import org.springframework.data.util.Optionals; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances. @@ -63,6 +71,7 @@ class RepositoryBeanDefinitionBuilder { private final MetadataReaderFactory metadataReaderFactory; private final FragmentMetadata fragmentMetadata; private final CustomRepositoryImplementationDetector implementationDetector; + private final RepositoryFactoriesLoader factoriesLoader; /** * Creates a new {@link RepositoryBeanDefinitionBuilder} from the given {@link BeanDefinitionRegistry}, @@ -83,7 +92,7 @@ public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, Reposito this.registry = registry; this.extension = extension; this.resourceLoader = resourceLoader; - + this.factoriesLoader = RepositoryFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader()); this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory); @@ -139,6 +148,7 @@ public BeanDefinitionBuilder build(RepositoryConfiguration configuration) { } // TODO: merge that with the one that creates the BD + // TODO: Add support for fragments discovered from spring.factories RepositoryConfigurationAdapter buildMetadata(RepositoryConfiguration configuration) { ImplementationDetectionConfiguration config = configuration @@ -223,21 +233,71 @@ private Stream registerRepositoryFragmentsImple ImplementationDetectionConfiguration config = configuration .toImplementationDetectionConfiguration(metadataReaderFactory); - return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) // - .map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) // - .flatMap(Optionals::toStream) // + Stream discovered = discoverFragments(configuration, config); + Stream loaded = loadFragments(configuration); + + return Stream.concat(discovered, loaded) // .peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) // .peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)); } + private Stream discoverFragments(RepositoryConfiguration configuration, + ImplementationDetectionConfiguration config) { + return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) + .map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) // + .flatMap(Optionals::toStream); + } + + private Stream loadFragments(RepositoryConfiguration configuration) { + + List names = factoriesLoader.loadFactoryNames(configuration.getRepositoryInterface()); + + if (names.isEmpty()) { + return Stream.empty(); + } + + return names.stream().map(it -> createFragmentConfiguration(null, configuration, it)); + } + private Optional detectRepositoryFragmentConfiguration(String fragmentInterface, ImplementationDetectionConfiguration config, RepositoryConfiguration configuration) { - ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface); - Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); + List names = factoriesLoader.loadFactoryNames(fragmentInterface); + + if (names.isEmpty()) { + + ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); + + return beanDefinition.map(bd -> createFragmentConfiguration(fragmentInterface, configuration, bd)); + } + + if (names.size() > 1) { + logger.debug(String.format("Multiple fragment implementations %s registered for fragment interface %s", names, + fragmentInterface)); + } + + return Optional.of(createFragmentConfiguration(fragmentInterface, configuration, names.get(0))); + } + + private RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface, + RepositoryConfiguration configuration, String className) { + + try { - return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd, - configuration.getConfigurationSource().generateBeanName(bd))); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className); + AnnotatedGenericBeanDefinition bd = new AnnotatedGenericBeanDefinition(metadataReader.getAnnotationMetadata()); + return createFragmentConfiguration(fragmentInterface, configuration, bd); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface, + RepositoryConfiguration configuration, AbstractBeanDefinition beanDefinition) { + + return new RepositoryFragmentConfiguration(fragmentInterface, beanDefinition, + configuration.getConfigurationSource().generateBeanName(beanDefinition)); } private String potentiallyRegisterRepositoryImplementation(RepositoryConfiguration configuration, @@ -314,10 +374,47 @@ private void potentiallyRegisterRepositoryFragment(RepositoryConfiguration co BeanDefinitionBuilder fragmentBuilder = BeanDefinitionBuilder.rootBeanDefinition(RepositoryFragment.class, "implemented"); - fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName()); + if (StringUtils.hasText(fragmentConfiguration.getInterfaceName())) { + fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName()); + } fragmentBuilder.addConstructorArgReference(fragmentConfiguration.getImplementationBeanName()); registry.registerBeanDefinition(beanName, ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource())); } + + static class RepositoryFactoriesLoader extends SpringFactoriesLoader { + + private final Map> factories; + + /** + * Create a new {@link SpringFactoriesLoader} instance. + * + * @param classLoader the classloader used to instantiate the factories + * @param factories a map of factory class name to implementation class names + */ + protected RepositoryFactoriesLoader(@Nullable ClassLoader classLoader, Map> factories) { + super(classLoader, factories); + this.factories = factories; + } + + /** + * Create a {@link RepositoryFactoriesLoader} instance that will load and instantiate the factory implementations + * from the default location, using the given class loader. + * + * @param classLoader the ClassLoader to use for loading resources; can be {@code null} to use the default + * @return a {@link RepositoryFactoriesLoader} instance + * @see #forResourceLocation(String) + */ + public static RepositoryFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) { + ClassLoader resourceClassLoader = (classLoader != null ? classLoader + : SpringFactoriesLoader.class.getClassLoader()); + return new RepositoryFactoriesLoader(classLoader, + loadFactoriesResource(resourceClassLoader, FACTORIES_RESOURCE_LOCATION)); + } + + List loadFactoryNames(String factoryType) { + return this.factories.getOrDefault(factoryType, Collections.emptyList()); + } + } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java index 6f24dec0e4..5db1dfcfb8 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.data.config.ConfigurationUtils; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -33,7 +34,7 @@ */ public final class RepositoryFragmentConfiguration { - private final String interfaceName; + private final Optional interfaceName; private final String className; private final Optional beanDefinition; private final String beanName; @@ -42,10 +43,10 @@ public final class RepositoryFragmentConfiguration { * Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@code className} of the * implementation. * - * @param interfaceName must not be {@literal null} or empty. + * @param interfaceName * @param className must not be {@literal null} or empty. */ - public RepositoryFragmentConfiguration(String interfaceName, String className) { + public RepositoryFragmentConfiguration(@Nullable String interfaceName, String className) { this(interfaceName, className, Optional.empty(), generateBeanName(className)); } @@ -53,33 +54,32 @@ public RepositoryFragmentConfiguration(String interfaceName, String className) { * Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@link AbstractBeanDefinition} of * the implementation. * - * @param interfaceName must not be {@literal null} or empty. + * @param interfaceName * @param beanDefinition must not be {@literal null}. */ - public RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition) { + public RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition) { - Assert.hasText(interfaceName, "Interface name must not be null or empty"); Assert.notNull(beanDefinition, "Bean definition must not be null"); - this.interfaceName = interfaceName; + this.interfaceName = Optional.ofNullable(interfaceName); this.className = ConfigurationUtils.getRequiredBeanClassName(beanDefinition); this.beanDefinition = Optional.of(beanDefinition); this.beanName = generateBeanName(); } - RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition, String beanName) { + RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition, + String beanName) { this(interfaceName, ConfigurationUtils.getRequiredBeanClassName(beanDefinition), Optional.of(beanDefinition), beanName); } - private RepositoryFragmentConfiguration(String interfaceName, String className, + private RepositoryFragmentConfiguration(@Nullable String interfaceName, String className, Optional beanDefinition, String beanName) { - Assert.hasText(interfaceName, "Interface name must not be null or empty"); Assert.notNull(beanDefinition, "Bean definition must not be null"); Assert.notNull(beanName, "Bean name must not be null"); - this.interfaceName = interfaceName; + this.interfaceName = Optional.ofNullable(interfaceName); this.className = className; this.beanDefinition = beanDefinition; this.beanName = beanName; @@ -107,8 +107,9 @@ public String getFragmentBeanName() { return getImplementationBeanName() + "Fragment"; } + @Nullable public String getInterfaceName() { - return this.interfaceName; + return this.interfaceName.orElse(null); } public String getClassName() { diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java index 7dd94aa721..0835bbeb4f 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; @@ -83,7 +84,8 @@ void shouldExposeFragmentsAsBean() { AnnotationMetadata metadata = new StandardAnnotationMetadata(SampleConfiguration.class, true); registrar.registerBeanDefinitions(metadata, registry); - verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"), any(BeanDefinition.class)); + verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"), + any(BeanDefinition.class)); } @Test // DATACMNS-1754 @@ -109,7 +111,7 @@ void registersBeanDefinitionWithoutFragmentImplementations() { assertNoBeanDefinitionRegisteredFor("excludedRepositoryImpl"); } - @Test // DATACMNS-1172 + @Test // DATACMNS-1172, GH-3090 void shouldLimitImplementationBasePackages() { AnnotationMetadata metadata = new StandardAnnotationMetadata(LimitsImplementationBasePackages.class, true); @@ -118,6 +120,8 @@ void shouldLimitImplementationBasePackages() { assertBeanDefinitionRegisteredFor("personRepository"); assertNoBeanDefinitionRegisteredFor("fragmentImpl"); + assertBeanDefinitionRegisteredFor("spiFragmentImplFragment"); + assertBeanDefinitionRegisteredFor("spiContribution"); } @Test // DATACMNS-360 diff --git a/src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java b/src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java index 6061d8f167..1c594d9171 100644 --- a/src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java +++ b/src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java @@ -17,8 +17,9 @@ import org.springframework.data.mapping.Person; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.spifragment.SpiFragment; /** * @author Mark Paluch */ -public interface PersonRepository extends Repository, Fragment {} +public interface PersonRepository extends Repository, Fragment, SpiFragment {} diff --git a/src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java b/src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java new file mode 100644 index 0000000000..8366801431 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java @@ -0,0 +1,23 @@ +/* + * 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.config.spifragment; + +/** + * Class included through spring.factories for PersonRepository. + * + * @author Mark Paluch + */ +public class SpiContribution {} diff --git a/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java b/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java new file mode 100644 index 0000000000..eff657885e --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java @@ -0,0 +1,21 @@ +/* + * 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.config.spifragment; + +/** + * @author Mark Paluch + */ +public interface SpiFragment {} diff --git a/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java b/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java new file mode 100644 index 0000000000..2db7796952 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java @@ -0,0 +1,23 @@ +/* + * 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.config.spifragment; + +/** + * Fragment for {@link SpiFragment} included through spring.factories for PersonRepository. + * + * @author Mark Paluch + */ +public class SpiFragmentImpl implements SpiFragment {} diff --git a/src/test/resources/META-INF/spring.factories b/src/test/resources/META-INF/spring.factories index 0e9bee7f8a..4a4a775e64 100644 --- a/src/test/resources/META-INF/spring.factories +++ b/src/test/resources/META-INF/spring.factories @@ -1,2 +1,4 @@ org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SampleMixin org.springframework.data.util.ProxyUtils$ProxyDetector=org.springframework.data.util.ProxyUtilsUnitTests$SampleProxyDetector +org.springframework.data.repository.config.basepackage.repo.PersonRepository=org.springframework.data.repository.config.spifragment.SpiContribution +org.springframework.data.repository.config.spifragment.SpiFragment=org.springframework.data.repository.config.spifragment.SpiFragmentImpl From b8a142c1aab9efcebded96f8e191a622724f336f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 2 Aug 2024 10:11:34 +0200 Subject: [PATCH 4/8] Hacking - Allow RepositoryMethodMetadata to be accessed during invocation. --- .../repositories/custom-implementations.adoc | 91 +++++++++++++++++++ .../DefaultRepositoryMethodMetadata.java | 79 ++++++++++++++++ .../QueryExecutorMethodInterceptor.java | 5 +- .../core/support/RepositoryComposition.java | 19 ++-- .../core/support/RepositoryMethodInvoker.java | 39 ++++++-- .../support/RepositoryMethodMetadata.java | 44 +++++++++ .../RepositoryCompositionUnitTests.java | 65 +++++++++++-- .../RepositoryMethodInvokerUnitTests.java | 29 +++--- 8 files changed, 335 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java create mode 100644 src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java 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 b4c84fcf00..8aa643643f 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -238,6 +238,97 @@ XML:: ====== ==== +[[repositories.spring-factories]] +==== Registering Fragments with spring.factories + +As already mentioned in the <> section, the infrastructure only auto detects fragments within the repositories base package. Therefore fragments residing in another location or maybe contributed by an external archive will not be found if they do not share a common namespace. +Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section. + +Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index. + +First all you need is the fragment interface. Please note the generic `` parameter to align the fragment with the repository domain type. + +==== +[source,java] +---- +package com.acme.search; + +public interface SearchExtension { + + List search(String text, Limit limit); +} +---- +==== + +Let's assume the actual full text search is available via a `SearchService` that is registered as a `Bean` within the context so we can consume it in our `SearchExtension` implementation. All we need to run the search is the collection/index name and a object mapper that converts the search results into actual domain objects as sketched out below. + +==== +[source,java] +---- +package com.acme.search; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Limit; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata; + +class DefaultSearchExtension implements SearchExtension { + + private SearchService service; + + DefaultSearchExtension(@Autowired SearchService service) { + this.service = service; + } + + public List search(String text, Limit limit) { + return search(RepositoryMethodMetadata.get(), text, limit); + } + + List search(RepositoryMethodMetadata metadata, String text, Limit limit) { + + Class domainType = metadata.repository().getDomainType(); + + String indexName = domainType.getSimpleName().toLowerCase(); + List jsonResult = service.search(indexName, text, 0, limit.max()); + + return jsonResult.stream().map( ... ).collect(toList()); + } +} +---- +==== + +In the snipped above we use `RepositoryMethodMetadata.get()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched. + +[TIP] +==== +For testing you can use `TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata)` to provide repository method metadata. +==== + +Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're good to go. + +==== +[source,properties] +---- +com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +---- +==== + +To make use of the extension simply add the interface to the repository as shown below. The infrastructure will take care placing the required `RepositoryMethodMetadata` so all that + +==== +[source,java] +---- +package io.my.movies; + +import com.acme.search.SearchExtension; +import org.springframework.data.repository.CrudRepository; + + +public interface MovieRepository extends CrudRepository, SearchExtension { + +} +---- +==== + [[repositories.customize-base-repository]] == Customize the Base Repository diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java new file mode 100644 index 0000000000..3a5b35f9a4 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java @@ -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.lang.reflect.Method; + +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Christoph Strobl + */ +class DefaultRepositoryMethodMetadata implements RepositoryMethodMetadata { + + private final RepositoryMetadata repositoryMetadata; + private final MethodMetadata methodMetadata; + + DefaultRepositoryMethodMetadata(RepositoryMetadata repositoryMetadata, MethodMetadata methodMetadata) { + + this.repositoryMetadata = repositoryMetadata; + this.methodMetadata = methodMetadata; + } + + static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, + Method declaredMethod) { + + return repositoryMethodMetadata(repositoryMetadata, declaredMethod, null); + } + + static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, + Method declaredMethod, @Nullable Method targetMethod) { + + return new DefaultRepositoryMethodMetadata(repositoryMetadata, + new DefaultMethodMetadata(declaredMethod, targetMethod)); + } + + static void bind(RepositoryMethodMetadata metadata) { + TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata); + } + + static void unbind() { + TransactionSynchronizationManager.unbindResourceIfPossible(RepositoryMethodMetadata.class); + } + + @Override + public RepositoryMetadata repository() { + return repositoryMetadata; + } + + @Override + public MethodMetadata method() { + return methodMetadata; + } + + @Override + public String toString() { + return "DefaultRepositoryMethodMetadata{" + "repository=" + repositoryMetadata.getRepositoryInterface() + + ", domainType=" + repositoryMetadata.getDomainType() + ", invokedMethod=" + methodMetadata.declaredMethod() + + ", targetMethod=" + methodMetadata.targetMethod() + '}'; + } + + record DefaultMethodMetadata(Method declaredMethod, @Nullable Method targetMethod) implements MethodMetadata { + } + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java index 13309fa6ac..135b0d2785 100644 --- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java @@ -33,6 +33,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethod; @@ -163,7 +164,9 @@ private Object doInvoke(MethodInvocation invocation) throws Throwable { RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method); if (invocationMetadata == null) { - invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method)); + + DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(repositoryInformation, method); + invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(repositoryMethodMetadata, queries.get(method)); invocationMetadataCache.put(method, invocationMetadata); } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index d26dedf258..2e01b208f1 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -32,6 +32,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.Streamable; @@ -281,7 +282,7 @@ Object invoke(RepositoryInvocationMulticaster listener, Method method, Object[] ReflectionUtils.makeAccessible(methodToCall); - return fragments.invoke(metadata != null ? metadata.getRepositoryInterface() : method.getDeclaringClass(), listener, + return fragments.invoke(metadata, listener, method, methodToCall, argumentConverter.apply(methodToCall, args)); } @@ -369,7 +370,6 @@ public static class RepositoryFragments implements Streamable> fragments; private RepositoryFragments(List> fragments) { - this.fragments = fragments; } @@ -382,6 +382,10 @@ public static RepositoryFragments empty() { return EMPTY; } + public static RepositoryFragments empty(RepositoryMetadata metadata) { + return EMPTY; + } + /** * Create {@link RepositoryFragments} from just implementation objects. * @@ -484,7 +488,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t /** * Invoke {@link Method} by resolving the fragment that implements a suitable method. * - * @param repositoryInterface + * @param metadata * @param listener * @param invokedMethod invoked method as per invocation on the interface. * @param methodToCall backend method that is backing the call. @@ -493,7 +497,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t * @throws Throwable */ @Nullable - Object invoke(Class repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod, + Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMulticaster listener, Method invokedMethod, Method methodToCall, Object[] args) throws Throwable { RepositoryFragment fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment); @@ -507,12 +511,15 @@ Object invoke(Class repositoryInterface, RepositoryInvocationMulticaster list if (repositoryMethodInvoker == null) { - repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(invokedMethod, optional.get(), + DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(metadata, invokedMethod, methodToCall); + repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(repositoryMethodMetadata, optional.get(), methodToCall); + invocationMetadataCache.put(invokedMethod, repositoryMethodInvoker); } - return repositoryMethodInvoker.invoke(repositoryInterface, listener, args); + Class target = (metadata != null && metadata.getRepositoryInterface() != null) ? metadata.getRepositoryInterface() : invokedMethod.getDeclaringClass(); + return repositoryMethodInvoker.invoke(target, listener, args); } private RepositoryFragment findImplementationFragment(Method key) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index 8647ba458a..e4ba94a011 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -18,12 +18,19 @@ import kotlin.Unit; import kotlin.reflect.KFunction; import kotlinx.coroutines.flow.Flow; +import org.springframework.core.type.MethodMetadata; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.util.TypeInformation; +import org.springframework.transaction.support.TransactionSynchronizationManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; +import java.util.Set; import java.util.stream.Stream; import org.reactivestreams.Publisher; @@ -46,8 +53,8 @@ * @author Mark Paluch * @author Christoph Strobl * @since 2.4 - * @see #forFragmentMethod(Method, Object, Method) - * @see #forRepositoryQuery(Method, RepositoryQuery) +// * @see #forFragmentMethod(Method, Object, Method) +// * @see #forRepositoryQuery(Method, RepositoryQuery) * @see RepositoryQuery * @see RepositoryComposition */ @@ -57,11 +64,13 @@ abstract class RepositoryMethodInvoker { private final Class returnedType; private final Invokable invokable; private final boolean suspendedDeclaredMethod; + protected RepositoryMethodMetadata repositoryMethodMetadata; @SuppressWarnings("ReactiveStreamsUnusedPublisher") - protected RepositoryMethodInvoker(Method method, Invokable invokable) { + protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetadata, Invokable invokable) { - this.method = method; + this.repositoryMethodMetadata = repositoryMethodMetadata; + this.method = repositoryMethodMetadata.method().declaredMethod(); if (KotlinDetector.isKotlinReflectPresent()) { @@ -116,7 +125,7 @@ protected RepositoryMethodInvoker(Method method, Invokable invokable) { } } - static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) { + static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata declaredMethod, RepositoryQuery query) { return new RepositoryQueryMethodInvoker(declaredMethod, query); } @@ -128,7 +137,7 @@ static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, Re * @param baseMethod the base method to call on fragment {@code instance}. * @return {@link RepositoryMethodInvoker} to call a fragment {@link Method}. */ - static RepositoryMethodInvoker forFragmentMethod(Method declaredMethod, Object instance, Method baseMethod) { + static RepositoryMethodInvoker forFragmentMethod(RepositoryMethodMetadata declaredMethod, Object instance, Method baseMethod) { return new RepositoryFragmentMethodInvoker(declaredMethod, instance, baseMethod); } @@ -167,6 +176,10 @@ private Object doInvoke(Class repositoryInterface, RepositoryInvocationMultic try { + if(RepositoryMethodMetadata.get() == null && repositoryMethodMetadata != null) { + DefaultRepositoryMethodMetadata.bind(repositoryMethodMetadata); + } + Object result = invokable.invoke(args); if (result != null && ReactiveWrappers.supports(result.getClass())) { @@ -184,6 +197,8 @@ private Object doInvoke(Class repositoryInterface, RepositoryInvocationMultic } catch (Exception e) { multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e))); throw e; + } finally { + DefaultRepositoryMethodMetadata.unbind(); } } @@ -202,7 +217,7 @@ interface Invokable { * Implementation to invoke query methods. */ private static class RepositoryQueryMethodInvoker extends RepositoryMethodInvoker { - public RepositoryQueryMethodInvoker(Method method, RepositoryQuery repositoryQuery) { + public RepositoryQueryMethodInvoker(RepositoryMethodMetadata method, RepositoryQuery repositoryQuery) { super(method, repositoryQuery::execute); } } @@ -255,15 +270,19 @@ Publisher decorate(Class repositoryInterface, RepositoryInvocationMul */ private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker { - public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) { - this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance, + public RepositoryFragmentMethodInvoker(RepositoryMethodMetadata metadata, Object instance, Method baseClassMethod) { + this(CoroutineAdapterInformation.create(metadata.method().declaredMethod(), baseClassMethod), metadata, instance, baseClassMethod); } - public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, Method declaredMethod, + public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, RepositoryMethodMetadata declaredMethod, Object instance, Method baseClassMethod) { super(declaredMethod, args -> { + try { + + + if (adapterInformation.shouldAdaptReactiveToSuspended()) { /* * Kotlin suspended functions are invoked with a synthetic Continuation parameter that keeps track of the Coroutine context. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java new file mode 100644 index 0000000000..6dd0cf178d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java @@ -0,0 +1,44 @@ +/* + * 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; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Christoph Strobl + */ +public interface RepositoryMethodMetadata { + + @Nullable + static RepositoryMethodMetadata get() { + return (RepositoryMethodMetadata) TransactionSynchronizationManager.getResource(RepositoryMethodMetadata.class); + } + + MethodMetadata method(); + + RepositoryMetadata repository(); + + interface MethodMetadata { + + Method declaredMethod(); + @Nullable Method targetMethod(); + } + +} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java index 8c6864e034..30322ef06d 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java @@ -15,8 +15,12 @@ */ package org.springframework.data.repository.core.support; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,8 +31,11 @@ import org.springframework.data.domain.Example; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -52,8 +59,7 @@ void before() { RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( new DefaultRepositoryMetadata(PersonRepository.class), backingRepo.getClass(), RepositoryComposition.empty()); - var mixin = RepositoryFragment.implemented(QueryByExampleExecutor.class, - queryByExampleExecutor); + var mixin = RepositoryFragment.implemented(QueryByExampleExecutor.class, queryByExampleExecutor); var base = RepositoryFragment.implemented(backingRepo); @@ -139,8 +145,7 @@ void shouldValidateStructuralFragments() { assertThatExceptionOfType(FragmentNotImplementedException.class) // .isThrownBy(mixed::validateImplementation) // - .withMessageContaining( - "Fragment org.springframework.data.repository.query.QueryByExampleExecutor") + .withMessageContaining("Fragment org.springframework.data.repository.query.QueryByExampleExecutor") .withMessageContaining("has no implementation"); } @@ -165,6 +170,33 @@ void shouldAppendCorrectly() { .containsSequence(initial, structural); } + @Test // GH-3090 + void fragmentInvocationProvidesRepositoryMethodMetadata() throws Throwable { + + RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( + new DefaultRepositoryMetadata(CapturingRepository.class), CapturingRepository.class, + RepositoryComposition.empty()); + + MethodMetadataCapturingMixin capturingMixin = new MethodMetadataCapturingMixin(); + RepositoryFragment foo = RepositoryFragment.implemented(capturingMixin); + + var fooBar = RepositoryComposition.of(RepositoryFragments.of(foo)) + .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)).withMetadata(repositoryInformation); + + var getString = ReflectionUtils.findMethod(CapturingRepository.class, "getString"); + + assertThat(getString).isNotNull(); + fooBar.invoke(fooBar.findMethod(getString).get()); + + RepositoryMethodMetadata lastValue = capturingMixin.getLastValue(); + assertThat(lastValue.repository()).isNotNull().extracting(RepositoryMetadata::getRepositoryInterface) + .isEqualTo(CapturingRepository.class); + + // TODO: I'm actually lost on that one + // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::declaredMethod).isEqualTo(getString); + // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::targetMethod).isEqualTo(FooMixin.class.getMethod("getString")); + } + interface PersonRepository extends Repository, QueryByExampleExecutor { Person save(Person entity); @@ -204,6 +236,10 @@ interface OrderedRepository extends Repository, FooMixin, BarMix } + interface CapturingRepository extends Repository, FooMixin { + + } + interface FooMixin { String getString(); @@ -218,6 +254,23 @@ public String getString() { } } + class MethodMetadataCapturingMixin implements FooMixin { + + List captured = new ArrayList<>(3); + + @Override + public String getString() { + + captured.add(RepositoryMethodMetadata.get()); + return FooMixinImpl.INSTANCE.getString(); + } + + @Nullable + RepositoryMethodMetadata getLastValue() { + return CollectionUtils.lastElement(captured); + } + } + interface BarMixin { String getString(); diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java index bc479f9dfb..e1d0d9ea4c 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java @@ -15,6 +15,18 @@ */ package org.springframework.data.repository.core.support; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.reactive.ReactiveFlowKt; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Iterator; @@ -26,8 +38,6 @@ import java.util.function.Consumer; import java.util.stream.Stream; -import kotlin.coroutines.Continuation; -import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.assertj.core.api.Assertions; import org.assertj.core.data.Percentage; import org.jetbrains.annotations.NotNull; @@ -39,26 +49,18 @@ import org.mockito.internal.stubbing.answers.Returns; import org.mockito.junit.jupiter.MockitoExtension; import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.CoroutineRepositoryMetadataUnitTests.MyCoroutineRepository; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * @author Christoph Strobl * @author Johannes Englmeier @@ -316,7 +318,8 @@ static class RepositoryMethodInvokerStub extends RepositoryMethodInvoker { RepositoryMethodInvokerStub(Class repositoryInterface, RepositoryInvocationMulticaster multicaster, String methodName, Invokable invokable) { - super(methodByName(repositoryInterface, methodName), invokable); + super(DefaultRepositoryMethodMetadata.repositoryMethodMetadata(mock(RepositoryMetadata.class), methodByName(repositoryInterface, methodName)), invokable); + this.repositoryInterface = repositoryInterface; this.multicaster = multicaster; } From 8f660f828833f317e5fc2fcfa692f9a266885ec7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 6 Aug 2024 09:10:40 +0200 Subject: [PATCH 5/8] Polishing. Rename RepositoryMethodMetadata to RepositoryMethodContext. Move RepositoryMethodContext to interceptor. Add flags to enable method metadata exposure. Expose MethodInvocation only if exposeMetadata is true. --- .../repositories/custom-implementations.adoc | 11 ++- .../DefaultRepositoryMethodContext.java | 75 +++++++++++++++ .../DefaultRepositoryMethodMetadata.java | 79 ---------------- .../QueryExecutorMethodInterceptor.java | 5 +- .../core/support/RepositoryComposition.java | 19 ++-- .../support/RepositoryFactoryBeanSupport.java | 14 +++ .../support/RepositoryFactorySupport.java | 46 ++++++++- .../core/support/RepositoryMethodContext.java | 93 +++++++++++++++++++ .../core/support/RepositoryMethodInvoker.java | 39 ++------ .../support/RepositoryMethodMetadata.java | 44 --------- .../RepositoryCompositionUnitTests.java | 59 +----------- .../RepositoryFactorySupportUnitTests.java | 54 ++++++++--- .../RepositoryMethodInvokerUnitTests.java | 13 +-- 13 files changed, 296 insertions(+), 255 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java delete mode 100644 src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java create mode 100644 src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java delete mode 100644 src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java 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 8aa643643f..94048b4327 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -269,7 +269,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.RepositoryMethodMetadata; +import org.springframework.data.repository.core.support.RepositoryMethodContext; class DefaultSearchExtension implements SearchExtension { @@ -280,12 +280,12 @@ class DefaultSearchExtension implements SearchExtension { } public List search(String text, Limit limit) { - return search(RepositoryMethodMetadata.get(), text, limit); + return search(RepositoryMethodContext.currentMethod(), text, limit); } - List search(RepositoryMethodMetadata metadata, String text, Limit limit) { + List search(RepositoryMethodContext metadata, String text, Limit limit) { - Class domainType = metadata.repository().getDomainType(); + Class domainType = metadata.getRepository().getDomainType(); String indexName = domainType.getSimpleName().toLowerCase(); List jsonResult = service.search(indexName, text, 0, limit.max()); @@ -312,7 +312,8 @@ com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension ---- ==== -To make use of the extension simply add the interface to the repository as shown below. The infrastructure will take care placing the required `RepositoryMethodMetadata` so all that +To make use of the extension simply add the interface to the repository as shown below. +The infrastructure will take care placing the required `RepositoryMethodContext` so all that ==== [source,java] 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 new file mode 100644 index 0000000000..5d1ed1e27d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodContext.java @@ -0,0 +1,75 @@ +/* + * 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.core.NamedThreadLocal; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.lang.Nullable; + +/** + * Class containing value objects providing information about the current repository method invocation. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +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"); + + private final RepositoryMetadata repositoryMetadata; + private final Method method; + + public DefaultRepositoryMethodContext(RepositoryMetadata repositoryMetadata, Method method) { + this.repositoryMetadata = repositoryMetadata; + this.method = method; + } + + @Nullable + public static RepositoryMethodContext getMetadata() { + return currentMethod.get(); + } + + @Nullable + public static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) { + + RepositoryMethodContext old = currentMethod.get(); + if (metadata != null) { + currentMethod.set(metadata); + } else { + currentMethod.remove(); + } + + return old; + } + + @Override + public RepositoryMetadata getRepository() { + return repositoryMetadata; + } + + @Override + public Method getMethod() { + return method; + } + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java deleted file mode 100644 index 3a5b35f9a4..0000000000 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryMethodMetadata.java +++ /dev/null @@ -1,79 +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; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** - * @author Christoph Strobl - */ -class DefaultRepositoryMethodMetadata implements RepositoryMethodMetadata { - - private final RepositoryMetadata repositoryMetadata; - private final MethodMetadata methodMetadata; - - DefaultRepositoryMethodMetadata(RepositoryMetadata repositoryMetadata, MethodMetadata methodMetadata) { - - this.repositoryMetadata = repositoryMetadata; - this.methodMetadata = methodMetadata; - } - - static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, - Method declaredMethod) { - - return repositoryMethodMetadata(repositoryMetadata, declaredMethod, null); - } - - static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata, - Method declaredMethod, @Nullable Method targetMethod) { - - return new DefaultRepositoryMethodMetadata(repositoryMetadata, - new DefaultMethodMetadata(declaredMethod, targetMethod)); - } - - static void bind(RepositoryMethodMetadata metadata) { - TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata); - } - - static void unbind() { - TransactionSynchronizationManager.unbindResourceIfPossible(RepositoryMethodMetadata.class); - } - - @Override - public RepositoryMetadata repository() { - return repositoryMetadata; - } - - @Override - public MethodMetadata method() { - return methodMetadata; - } - - @Override - public String toString() { - return "DefaultRepositoryMethodMetadata{" + "repository=" + repositoryMetadata.getRepositoryInterface() - + ", domainType=" + repositoryMetadata.getDomainType() + ", invokedMethod=" + methodMetadata.declaredMethod() - + ", targetMethod=" + methodMetadata.targetMethod() + '}'; - } - - record DefaultMethodMetadata(Method declaredMethod, @Nullable Method targetMethod) implements MethodMetadata { - } - -} diff --git a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java index 135b0d2785..13309fa6ac 100644 --- a/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java +++ b/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java @@ -33,7 +33,6 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; -import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethod; @@ -164,9 +163,7 @@ private Object doInvoke(MethodInvocation invocation) throws Throwable { RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method); if (invocationMetadata == null) { - - DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(repositoryInformation, method); - invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(repositoryMethodMetadata, queries.get(method)); + invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method)); invocationMetadataCache.put(method, invocationMetadata); } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index 2e01b208f1..d26dedf258 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -32,7 +32,6 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; -import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.Streamable; @@ -282,7 +281,7 @@ Object invoke(RepositoryInvocationMulticaster listener, Method method, Object[] ReflectionUtils.makeAccessible(methodToCall); - return fragments.invoke(metadata, listener, + return fragments.invoke(metadata != null ? metadata.getRepositoryInterface() : method.getDeclaringClass(), listener, method, methodToCall, argumentConverter.apply(methodToCall, args)); } @@ -370,6 +369,7 @@ public static class RepositoryFragments implements Streamable> fragments; private RepositoryFragments(List> fragments) { + this.fragments = fragments; } @@ -382,10 +382,6 @@ public static RepositoryFragments empty() { return EMPTY; } - public static RepositoryFragments empty(RepositoryMetadata metadata) { - return EMPTY; - } - /** * Create {@link RepositoryFragments} from just implementation objects. * @@ -488,7 +484,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t /** * Invoke {@link Method} by resolving the fragment that implements a suitable method. * - * @param metadata + * @param repositoryInterface * @param listener * @param invokedMethod invoked method as per invocation on the interface. * @param methodToCall backend method that is backing the call. @@ -497,7 +493,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t * @throws Throwable */ @Nullable - Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMulticaster listener, Method invokedMethod, + Object invoke(Class repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod, Method methodToCall, Object[] args) throws Throwable { RepositoryFragment fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment); @@ -511,15 +507,12 @@ Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMultica if (repositoryMethodInvoker == null) { - DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(metadata, invokedMethod, methodToCall); - repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(repositoryMethodMetadata, optional.get(), + repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(invokedMethod, optional.get(), methodToCall); - invocationMetadataCache.put(invokedMethod, repositoryMethodInvoker); } - Class target = (metadata != null && metadata.getRepositoryInterface() != null) ? metadata.getRepositoryInterface() : invokedMethod.getDeclaringClass(); - return repositoryMethodInvoker.invoke(target, listener, args); + return repositoryMethodInvoker.invoke(repositoryInterface, listener, args); } private RepositoryFragment findImplementationFragment(Method key) { 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 f3e6c13dae..950b3cba93 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 @@ -69,6 +69,7 @@ public abstract class RepositoryFactoryBeanSupport, private final Class repositoryInterface; private RepositoryFactorySupport factory; + private boolean exposeMetadata; private Key queryLookupStrategyKey; private Optional> repositoryBaseClass = Optional.empty(); private Optional customImplementation = Optional.empty(); @@ -107,6 +108,18 @@ public void setRepositoryBaseClass(Class repositoryBaseClass) { this.repositoryBaseClass = Optional.ofNullable(repositoryBaseClass); } + /** + * Set whether the repository method metadata should be exposed by the repository factory as a ThreadLocal for + * retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain + * repository information. + *

+ * 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. + */ + public void setExposeMetadata(boolean exposeMetadata) { + this.exposeMetadata = exposeMetadata; + } + /** * Set the {@link QueryLookupStrategy.Key} to be used. * @@ -258,6 +271,7 @@ public boolean isSingleton() { public void afterPropertiesSet() { this.factory = createRepositoryFactory(); + this.factory.setExposeMetadata(exposeMetadata); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); this.factory.setEvaluationContextProvider( 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 b3babf4659..4a026beae8 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.repository.core.support; +import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; @@ -29,6 +30,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jetbrains.annotations.NotNull; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; @@ -94,6 +96,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, private final List postProcessors; private Optional> repositoryBaseClass; + private boolean exposeMetadata; private @Nullable QueryLookupStrategy.Key queryLookupStrategyKey; private List> queryPostProcessors; private List methodInvocationListeners; @@ -121,6 +124,18 @@ public RepositoryFactorySupport() { this.projectionFactory = createProjectionFactory(); } + /** + * Set whether the repository method metadata should be exposed by the repository factory as a ThreadLocal for + * retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain + * repository information. + *

+ * 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. + */ + public void setExposeMetadata(boolean exposeMetadata) { + this.exposeMetadata = exposeMetadata; + } + /** * Sets the strategy of how to lookup a query to execute finders. * @@ -330,7 +345,10 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra result.addAdvice(new MethodInvocationValidator()); } - result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + if (this.exposeMetadata) { + result.addAdvice(new ExposeMetadataInterceptor(metadata)); + result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + } if (!postProcessors.isEmpty()) { StartupStep repositoryPostprocessorsStep = onEvent(applicationStartup, "spring.data.repository.postprocessors", @@ -634,6 +652,32 @@ public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) thro } } + /** + * Interceptor for repository proxies when the repository needs exposing metadata. + */ + private static class ExposeMetadataInterceptor implements MethodInterceptor, Serializable { + + private final RepositoryMetadata repositoryMetadata; + + public ExposeMetadataInterceptor(RepositoryMetadata repositoryMetadata) { + this.repositoryMetadata = repositoryMetadata; + } + + @Nullable + @Override + public Object invoke(@NotNull MethodInvocation invocation) throws Throwable { + RepositoryMethodContext oldMetadata = null; + try { + oldMetadata = RepositoryMethodContext + .setCurrentMetadata(new DefaultRepositoryMethodContext(repositoryMetadata, invocation.getMethod())); + return invocation.proceed(); + } finally { + RepositoryMethodContext.setCurrentMetadata(oldMetadata); + } + } + + } + /** * {@link QueryCreationListener} collecting the {@link QueryMethod}s created for all query methods of the repository * interface. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java new file mode 100644 index 0000000000..4c28b0ef81 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java @@ -0,0 +1,93 @@ +/* + * 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. + *

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

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

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

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

+ * 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(); + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index e4ba94a011..8647ba458a 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -18,19 +18,12 @@ import kotlin.Unit; import kotlin.reflect.KFunction; import kotlinx.coroutines.flow.Flow; -import org.springframework.core.type.MethodMetadata; -import org.springframework.data.repository.core.CrudMethods; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.util.TypeInformation; -import org.springframework.transaction.support.TransactionSynchronizationManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; -import java.util.Set; import java.util.stream.Stream; import org.reactivestreams.Publisher; @@ -53,8 +46,8 @@ * @author Mark Paluch * @author Christoph Strobl * @since 2.4 -// * @see #forFragmentMethod(Method, Object, Method) -// * @see #forRepositoryQuery(Method, RepositoryQuery) + * @see #forFragmentMethod(Method, Object, Method) + * @see #forRepositoryQuery(Method, RepositoryQuery) * @see RepositoryQuery * @see RepositoryComposition */ @@ -64,13 +57,11 @@ abstract class RepositoryMethodInvoker { private final Class returnedType; private final Invokable invokable; private final boolean suspendedDeclaredMethod; - protected RepositoryMethodMetadata repositoryMethodMetadata; @SuppressWarnings("ReactiveStreamsUnusedPublisher") - protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetadata, Invokable invokable) { + protected RepositoryMethodInvoker(Method method, Invokable invokable) { - this.repositoryMethodMetadata = repositoryMethodMetadata; - this.method = repositoryMethodMetadata.method().declaredMethod(); + this.method = method; if (KotlinDetector.isKotlinReflectPresent()) { @@ -125,7 +116,7 @@ protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetad } } - static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata declaredMethod, RepositoryQuery query) { + static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) { return new RepositoryQueryMethodInvoker(declaredMethod, query); } @@ -137,7 +128,7 @@ static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata * @param baseMethod the base method to call on fragment {@code instance}. * @return {@link RepositoryMethodInvoker} to call a fragment {@link Method}. */ - static RepositoryMethodInvoker forFragmentMethod(RepositoryMethodMetadata declaredMethod, Object instance, Method baseMethod) { + static RepositoryMethodInvoker forFragmentMethod(Method declaredMethod, Object instance, Method baseMethod) { return new RepositoryFragmentMethodInvoker(declaredMethod, instance, baseMethod); } @@ -176,10 +167,6 @@ private Object doInvoke(Class repositoryInterface, RepositoryInvocationMultic try { - if(RepositoryMethodMetadata.get() == null && repositoryMethodMetadata != null) { - DefaultRepositoryMethodMetadata.bind(repositoryMethodMetadata); - } - Object result = invokable.invoke(args); if (result != null && ReactiveWrappers.supports(result.getClass())) { @@ -197,8 +184,6 @@ private Object doInvoke(Class repositoryInterface, RepositoryInvocationMultic } catch (Exception e) { multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e))); throw e; - } finally { - DefaultRepositoryMethodMetadata.unbind(); } } @@ -217,7 +202,7 @@ interface Invokable { * Implementation to invoke query methods. */ private static class RepositoryQueryMethodInvoker extends RepositoryMethodInvoker { - public RepositoryQueryMethodInvoker(RepositoryMethodMetadata method, RepositoryQuery repositoryQuery) { + public RepositoryQueryMethodInvoker(Method method, RepositoryQuery repositoryQuery) { super(method, repositoryQuery::execute); } } @@ -270,19 +255,15 @@ Publisher decorate(Class repositoryInterface, RepositoryInvocationMul */ private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker { - public RepositoryFragmentMethodInvoker(RepositoryMethodMetadata metadata, Object instance, Method baseClassMethod) { - this(CoroutineAdapterInformation.create(metadata.method().declaredMethod(), baseClassMethod), metadata, instance, + public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) { + this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance, baseClassMethod); } - public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, RepositoryMethodMetadata declaredMethod, + public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, Method declaredMethod, Object instance, Method baseClassMethod) { super(declaredMethod, args -> { - try { - - - if (adapterInformation.shouldAdaptReactiveToSuspended()) { /* * Kotlin suspended functions are invoked with a synthetic Continuation parameter that keeps track of the Coroutine context. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java deleted file mode 100644 index 6dd0cf178d..0000000000 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodMetadata.java +++ /dev/null @@ -1,44 +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; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -/** - * @author Christoph Strobl - */ -public interface RepositoryMethodMetadata { - - @Nullable - static RepositoryMethodMetadata get() { - return (RepositoryMethodMetadata) TransactionSynchronizationManager.getResource(RepositoryMethodMetadata.class); - } - - MethodMetadata method(); - - RepositoryMetadata repository(); - - interface MethodMetadata { - - Method declaredMethod(); - @Nullable Method targetMethod(); - } - -} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java index 30322ef06d..3181ed27b1 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryCompositionUnitTests.java @@ -15,12 +15,8 @@ */ package org.springframework.data.repository.core.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.List; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,11 +27,8 @@ import org.springframework.data.domain.Example; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -170,33 +163,6 @@ void shouldAppendCorrectly() { .containsSequence(initial, structural); } - @Test // GH-3090 - void fragmentInvocationProvidesRepositoryMethodMetadata() throws Throwable { - - RepositoryInformation repositoryInformation = new DefaultRepositoryInformation( - new DefaultRepositoryMetadata(CapturingRepository.class), CapturingRepository.class, - RepositoryComposition.empty()); - - MethodMetadataCapturingMixin capturingMixin = new MethodMetadataCapturingMixin(); - RepositoryFragment foo = RepositoryFragment.implemented(capturingMixin); - - var fooBar = RepositoryComposition.of(RepositoryFragments.of(foo)) - .withMethodLookup(MethodLookups.forRepositoryTypes(repositoryInformation)).withMetadata(repositoryInformation); - - var getString = ReflectionUtils.findMethod(CapturingRepository.class, "getString"); - - assertThat(getString).isNotNull(); - fooBar.invoke(fooBar.findMethod(getString).get()); - - RepositoryMethodMetadata lastValue = capturingMixin.getLastValue(); - assertThat(lastValue.repository()).isNotNull().extracting(RepositoryMetadata::getRepositoryInterface) - .isEqualTo(CapturingRepository.class); - - // TODO: I'm actually lost on that one - // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::declaredMethod).isEqualTo(getString); - // assertThat(lastValue.method()).isNotNull().extracting(MethodMetadata::targetMethod).isEqualTo(FooMixin.class.getMethod("getString")); - } - interface PersonRepository extends Repository, QueryByExampleExecutor { Person save(Person entity); @@ -236,10 +202,6 @@ interface OrderedRepository extends Repository, FooMixin, BarMix } - interface CapturingRepository extends Repository, FooMixin { - - } - interface FooMixin { String getString(); @@ -254,23 +216,6 @@ public String getString() { } } - class MethodMetadataCapturingMixin implements FooMixin { - - List captured = new ArrayList<>(3); - - @Override - public String getString() { - - captured.add(RepositoryMethodMetadata.get()); - return FooMixinImpl.INSTANCE.getString(); - } - - @Nullable - RepositoryMethodMetadata getLastValue() { - return CollectionUtils.lastElement(captured); - } - } - interface BarMixin { String getString(); 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 71a4702823..79f2862bf9 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 @@ -30,6 +30,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,6 +41,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; @@ -117,7 +119,7 @@ void invokesCustomQueryCreationListenerForSpecialRepositoryQueryOnly() { factory.getRepository(ObjectRepository.class); verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class)); - verify(otherListener, times(2)).onCreation(any(RepositoryQuery.class)); + verify(otherListener, times(3)).onCreation(any(RepositoryQuery.class)); } @Test // DATACMNS-1538 @@ -154,8 +156,7 @@ void invokesCustomMethodIfItRedeclaresACRUDOne() { @Test // DATACMNS-102 void invokesCustomMethodCompositionMethodIfItRedeclaresACRUDOne() { - var repository = factory.getRepository(ObjectRepository.class, - RepositoryFragments.just(customImplementation)); + var repository = factory.getRepository(ObjectRepository.class, RepositoryFragments.just(customImplementation)); repository.findById(1); verify(customImplementation, times(1)).findById(1); @@ -247,6 +248,29 @@ void capturesFailureFromInvocation() { assertThat(repositoryMethodInvocation.getResult().getError()).isInstanceOf(IllegalStateException.class); } + @Test // GH-3090 + void capturesRepositoryMetadata() { + + record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) { + } + + when(factory.queryOne.execute(any(Object[].class))) + .then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(), + ExposeInvocationInterceptor.currentInvocation())); + + factory.setExposeMetadata(true); + + var repository = factory.getRepository(ObjectRepository.class); + var metadataByLastname = repository.findMetadataByLastname(); + + assertThat(metadataByLastname).isInstanceOf(Metadata.class); + + Metadata metadata = (Metadata) metadataByLastname; + assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname"); + assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class); + assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname"); + } + @Test // DATACMNS-509, DATACMNS-1764 void convertsWithSameElementType() { @@ -283,8 +307,8 @@ void rejectsNullRepositoryProxyPostProcessor() { assertThatThrownBy( // () -> factory.addRepositoryProxyPostProcessor(null)) // - .isInstanceOf(IllegalArgumentException.class) // - .hasMessageContaining(RepositoryProxyPostProcessor.class.getSimpleName()); + .isInstanceOf(IllegalArgumentException.class) // + .hasMessageContaining(RepositoryProxyPostProcessor.class.getSimpleName()); } @Test // DATACMNS-715, SPR-13109 @@ -334,9 +358,9 @@ void rejectsRepositoryBaseClassWithInvalidConstructor() { assertThatThrownBy( // () -> factory.getTargetRepositoryViaReflection(information, entityInformation, "Foo")) // - .isInstanceOf(IllegalStateException.class) // - .hasMessageContaining(entityInformation.getClass().getName()) // - .hasMessageContaining(String.class.getName()); + .isInstanceOf(IllegalStateException.class) // + .hasMessageContaining(entityInformation.getClass().getName()) // + .hasMessageContaining(String.class.getName()); } @Test @@ -357,8 +381,8 @@ void considersRequiredReturnValue() { assertThatThrownBy( // () -> repository.findById("")) // - .isInstanceOf(EmptyResultDataAccessException.class) // - .hasMessageContaining("Result must not be null"); + .isInstanceOf(EmptyResultDataAccessException.class) // + .hasMessageContaining("Result must not be null"); assertThat(repository.findByUsername("")).isNull(); } @@ -370,8 +394,8 @@ void considersRequiredParameter() { assertThatThrownBy( // () -> repository.findByClass(null)) // - .isInstanceOf(IllegalArgumentException.class) // - .hasMessageContaining("must not be null"); + .isInstanceOf(IllegalArgumentException.class) // + .hasMessageContaining("must not be null"); } @Test // DATACMNS-1154 @@ -391,8 +415,8 @@ void considersRequiredKotlinParameter() { assertThatThrownBy( // () -> repository.findById(null)) // - .isInstanceOf(IllegalArgumentException.class) // - .hasMessageContaining("must not be null"); // + .isInstanceOf(IllegalArgumentException.class) // + .hasMessageContaining("must not be null"); // } @Test // DATACMNS-1154 @@ -509,6 +533,8 @@ interface ObjectRepository extends Repository, ObjectRepositoryC @Nullable Object save(Object entity); + Object findMetadataByLastname(); + static String staticMethod() { return "OK"; } diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java index e1d0d9ea4c..c1e68a4bd3 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryMethodInvokerUnitTests.java @@ -15,11 +15,8 @@ */ package org.springframework.data.repository.core.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import kotlin.coroutines.Continuation; import kotlinx.coroutines.reactive.ReactiveFlowKt; @@ -49,12 +46,11 @@ import org.mockito.internal.stubbing.answers.Returns; import org.mockito.junit.jupiter.MockitoExtension; import org.reactivestreams.Subscription; + import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.CoroutineRepositoryMetadataUnitTests.MyCoroutineRepository; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; -import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.lang.Nullable; @@ -318,8 +314,7 @@ static class RepositoryMethodInvokerStub extends RepositoryMethodInvoker { RepositoryMethodInvokerStub(Class repositoryInterface, RepositoryInvocationMulticaster multicaster, String methodName, Invokable invokable) { - super(DefaultRepositoryMethodMetadata.repositoryMethodMetadata(mock(RepositoryMetadata.class), methodByName(repositoryInterface, methodName)), invokable); - + super(methodByName(repositoryInterface, methodName), invokable); this.repositoryInterface = repositoryInterface; this.multicaster = multicaster; } From 58607f5af23bc4ea1ce4964c655140f48de26181 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 Aug 2024 13:54:40 +0200 Subject: [PATCH 6/8] Update javadoc tags & remove non spring nullable annotation. --- .../core/support/DefaultRepositoryMethodContext.java | 8 +++++--- .../core/support/RepositoryFactoryBeanSupport.java | 2 ++ .../core/support/RepositoryFactorySupport.java | 9 +++++---- .../repository/core/support/RepositoryMethodContext.java | 1 + 4 files changed, 13 insertions(+), 7 deletions(-) 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 5d1ed1e27d..8f87c16785 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 @@ -26,6 +26,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @since 3.4.0 */ class DefaultRepositoryMethodContext implements RepositoryMethodContext { @@ -39,18 +40,19 @@ class DefaultRepositoryMethodContext implements RepositoryMethodContext { private final RepositoryMetadata repositoryMetadata; private final Method method; - public DefaultRepositoryMethodContext(RepositoryMetadata repositoryMetadata, Method method) { + DefaultRepositoryMethodContext(RepositoryMetadata repositoryMetadata, Method method) { + this.repositoryMetadata = repositoryMetadata; this.method = method; } @Nullable - public static RepositoryMethodContext getMetadata() { + static RepositoryMethodContext getMetadata() { return currentMethod.get(); } @Nullable - public static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) { + static RepositoryMethodContext setMetadata(@Nullable RepositoryMethodContext metadata) { RepositoryMethodContext old = currentMethod.get(); if (metadata != null) { 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 950b3cba93..b1a8f155ff 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 @@ -115,6 +115,8 @@ 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 */ 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 4a026beae8..88965a4abe 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 @@ -30,8 +30,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.jetbrains.annotations.NotNull; - import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.beans.BeanUtils; @@ -129,8 +127,10 @@ public RepositoryFactorySupport() { * retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain * repository information. *

- * Default is "false", in order to avoid unnecessary extra interception. This means that no guarantees are provided + * Default is {@literal "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 */ public void setExposeMetadata(boolean exposeMetadata) { this.exposeMetadata = exposeMetadata; @@ -665,7 +665,8 @@ public ExposeMetadataInterceptor(RepositoryMetadata repositoryMetadata) { @Nullable @Override - public Object invoke(@NotNull MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) throws Throwable { + RepositoryMethodContext oldMetadata = null; try { oldMetadata = RepositoryMethodContext diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java index 4c28b0ef81..516c8d95b6 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodContext.java @@ -35,6 +35,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @since 3.4.0 */ public interface RepositoryMethodContext { From c8dd57ad2daaa1a3e54672a7a3592559d1d3f30b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 Aug 2024 15:26:56 +0200 Subject: [PATCH 7/8] Update documentation. --- .../repositories/custom-implementations.adoc | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) 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 94048b4327..a3867d5119 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -296,24 +296,52 @@ class DefaultSearchExtension implements SearchExtension { ---- ==== -In the snipped above we use `RepositoryMethodMetadata.get()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched. +In the snipped above we use `RepositoryMethodContext.currentMethod()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched. + +Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're almost good to go. -[TIP] ==== -For testing you can use `TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata)` to provide repository method metadata. +[source,properties] +---- +com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +---- ==== -Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're good to go. +Since we're using additional metadata, that comes with additional cost, via `RepositoryMethodContext.currentMethod()` we need to advise the repository factory responsible for creating the actual repository to expose method metadata by setting the `exposeMetadata` flag. ==== -[source,properties] +[source,java] ---- -com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.lang.Nullable; + +@Configuration +class Cfg implements BeanPostProcessor { + + @Bean + public BeanPostProcessor exposeMethodMetadata() { + + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + + if(bean instanceof RepositoryFactoryBeanSupport factoryBean) { + factoryBean.setExposeMetadata(true); + } + } + }; + } +} ---- +The above snippet outlines how to set the `exposeMetadata` flag using a `BeanPostProcessor`. +Please do not just copy/paste the above but think about your actual use case which may require a more fine grained approach as the above will simply enable the flag on each and every repository. You may want to have a look at our https://github.com/spring-projects/spring-data-examples/tree/main/bom[spring-data-examples] project to draw inspiration. ==== -To make use of the extension simply add the interface to the repository as shown below. -The infrastructure will take care placing the required `RepositoryMethodContext` so all that +Now we are ready to make use of the extension. +Simply add the interface to the repository. ==== [source,java] From d9ddddce7f709b1f38c5e3ac2a97a9077383dcb9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 Aug 2024 09:41:55 +0200 Subject: [PATCH 8/8] Polishing. Refine grammar, line breaks, typos. --- .../repositories/custom-implementations.adoc | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) 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 a3867d5119..e1f66a2f38 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -241,12 +241,14 @@ XML:: [[repositories.spring-factories]] ==== Registering Fragments with spring.factories -As already mentioned in the <> section, the infrastructure only auto detects fragments within the repositories base package. Therefore fragments residing in another location or maybe contributed by an external archive will not be found if they do not share a common namespace. +As already mentioned in the <> section, the infrastructure only auto-detects fragments within the repository base-package. +Therefore, fragments residing in another location or want to be contributed by an external archive will not be found if they do not share a common namespace. Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section. Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index. -First all you need is the fragment interface. Please note the generic `` parameter to align the fragment with the repository domain type. +First all you need is the fragment interface. +Note the generic `` parameter to align the fragment with the repository domain type. ==== [source,java] @@ -260,7 +262,8 @@ public interface SearchExtension { ---- ==== -Let's assume the actual full text search is available via a `SearchService` that is registered as a `Bean` within the context so we can consume it in our `SearchExtension` implementation. All we need to run the search is the collection/index name and a object mapper that converts the search results into actual domain objects as sketched out below. +Let's assume the actual full-text search is available via a `SearchService` that is registered as a `Bean` within the context so you can consume it in our `SearchExtension` implementation. +All you need to run the search is the collection (or index) name and an object mapper that converts the search results into actual domain objects as sketched out below. ==== [source,java] @@ -273,9 +276,9 @@ import org.springframework.data.repository.core.support.RepositoryMethodContext; class DefaultSearchExtension implements SearchExtension { - private SearchService service; + private final SearchService service; - DefaultSearchExtension(@Autowired SearchService service) { + DefaultSearchExtension(SearchService service) { this.service = service; } @@ -290,16 +293,19 @@ class DefaultSearchExtension implements SearchExtension { String indexName = domainType.getSimpleName().toLowerCase(); List jsonResult = service.search(indexName, text, 0, limit.max()); - return jsonResult.stream().map( ... ).collect(toList()); + return jsonResult.stream().map(…).collect(toList()); } } ---- ==== -In the snipped above we use `RepositoryMethodContext.currentMethod()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched. +In the example above `RepositoryMethodContext.currentMethod()` 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. -Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're almost good to go. +Now that you've got both, the fragment declaration and implementation you can register it in the `META-INF/spring.factories` file, package things up if needed, and you're good to go. +.Registering a fragment implementation through `META-INF/spring.factories` ==== [source,properties] ---- @@ -307,7 +313,8 @@ com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension ---- ==== -Since we're using additional metadata, that comes with additional cost, via `RepositoryMethodContext.currentMethod()` we need to advise the repository factory responsible for creating the actual repository to expose method metadata by setting the `exposeMetadata` flag. +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 by setting the `exposeMetadata` flag. ==== [source,java] @@ -318,10 +325,10 @@ import org.springframework.data.repository.core.support.RepositoryFactoryBeanSup import org.springframework.lang.Nullable; @Configuration -class Cfg implements BeanPostProcessor { +class MyConfiguration implements BeanPostProcessor { @Bean - public BeanPostProcessor exposeMethodMetadata() { + static BeanPostProcessor exposeMethodMetadata() { return new BeanPostProcessor() { @@ -336,12 +343,13 @@ class Cfg implements BeanPostProcessor { } } ---- -The above snippet outlines how to set the `exposeMetadata` flag using a `BeanPostProcessor`. -Please do not just copy/paste the above but think about your actual use case which may require a more fine grained approach as the above will simply enable the flag on each and every repository. You may want to have a look at our https://github.com/spring-projects/spring-data-examples/tree/main/bom[spring-data-examples] project to draw inspiration. + +The above example outlines how to enable metadata exposure by setting the `exposeMetadata` flag using a `BeanPostProcessor`. +Please do not just copy/paste the above but consider your actual use case which may require a more fine-grained approach as the above will simply enable the flag on every repository. +You may want to have a look at our https://github.com/spring-projects/spring-data-examples/tree/main/bom[spring-data-examples] project to draw inspiration. ==== -Now we are ready to make use of the extension. -Simply add the interface to the repository. +Now you are ready to make use of your extension; Simply add the interface to your repository: ==== [source,java] @@ -351,8 +359,7 @@ package io.my.movies; import com.acme.search.SearchExtension; import org.springframework.data.repository.CrudRepository; - -public interface MovieRepository extends CrudRepository, SearchExtension { +interface MovieRepository extends CrudRepository, SearchExtension { } ----