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 b9c70f087..c23f54fc0 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 static org.springframework.beans.factory.config.BeanDefinition.*; 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.BeanDefinitionReaderUtils; 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 @@ class RepositoryBeanDefinitionBuilder { 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 @@ class RepositoryBeanDefinitionBuilder { } // 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 @@ class RepositoryBeanDefinitionBuilder { 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 @@ class RepositoryBeanDefinitionBuilder { 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 6f24dec0e..5db1dfcfb 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 java.util.Optional; 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 @@ import org.springframework.util.ObjectUtils; */ 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 final class RepositoryFragmentConfiguration { * 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 final class RepositoryFragmentConfiguration { 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 7dd94aa72..0835bbeb4 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.Test; 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 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests { 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 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests { assertNoBeanDefinitionRegisteredFor("excludedRepositoryImpl"); } - @Test // DATACMNS-1172 + @Test // DATACMNS-1172, GH-3090 void shouldLimitImplementationBasePackages() { AnnotationMetadata metadata = new StandardAnnotationMetadata(LimitsImplementationBasePackages.class, true); @@ -118,6 +120,8 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests { 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 6061d8f16..1c594d917 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 @@ package org.springframework.data.repository.config.basepackage.repo; 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 000000000..836680143 --- /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 000000000..eff657885 --- /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 000000000..2db779695 --- /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 0e9bee7f8..4a4a775e6 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