diff --git a/pom.xml b/pom.xml index 75f26f703f..23d7707b15 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-commons - 3.0.0-SNAPSHOT + 3.0.0-GH-2487-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java index 7be0197916..dc105616a4 100644 --- a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java +++ b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.beans.factory.config.BeanDefinition; @@ -107,8 +108,22 @@ public Optional detectCustomImplementation(Implementatio .filter(lookup::matches) // .collect(StreamUtils.toUnmodifiableSet()); + return selectImplementationCandidate(lookup, definitions, () -> { + + if (definitions.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(definitions.iterator().next()); + }); + } + + private static Optional selectImplementationCandidate( + ImplementationLookupConfiguration lookup, Set definitions, + Supplier> fallback) { + return SelectionSet // - .of(definitions, c -> c.isEmpty() ? Optional.empty() : throwAmbiguousCustomImplementationException(c)) // + .of(definitions, c -> c.isEmpty() ? fallback.get() : throwAmbiguousCustomImplementationException(c)) // .filterIfNecessary(lookup::hasMatchingBeanName) // .uniqueResult() // .map(AbstractBeanDefinition.class::cast); diff --git a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java index 9c7eff7c42..4b5b3e512e 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java @@ -15,7 +15,6 @@ */ package org.springframework.data.repository.config; -import java.beans.Introspector; import java.io.IOException; import org.springframework.beans.factory.config.BeanDefinition; @@ -42,22 +41,16 @@ class DefaultImplementationLookupConfiguration implements ImplementationLookupCo private final String interfaceName; private final String beanName; - /** - * Creates a new {@link DefaultImplementationLookupConfiguration} for the given - * {@link ImplementationDetectionConfiguration} and interface name. - * - * @param config must not be {@literal null}. - * @param interfaceName must not be {@literal null} or empty. - */ - DefaultImplementationLookupConfiguration(ImplementationDetectionConfiguration config, String interfaceName) { + DefaultImplementationLookupConfiguration(ImplementationDetectionConfiguration config, String interfaceName, + String beanName) { Assert.notNull(config, "ImplementationDetectionConfiguration must not be null"); Assert.hasText(interfaceName, "Interface name must not be null or empty"); + Assert.hasText(beanName, "Bean name must not be null or empty!"); this.config = config; this.interfaceName = interfaceName; - this.beanName = Introspector - .decapitalize(ClassUtils.getShortName(interfaceName).concat(config.getImplementationPostfix())); + this.beanName = beanName; } @Override diff --git a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java index 623c94f2f8..bf13ab9d71 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.config.ConfigurationUtils; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.util.Lazy; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -45,6 +46,7 @@ public class DefaultRepositoryConfiguration beanName; public DefaultRepositoryConfiguration(T configurationSource, BeanDefinition definition, RepositoryConfigurationExtension extension) { @@ -52,6 +54,7 @@ public DefaultRepositoryConfiguration(T configurationSource, BeanDefinition defi this.configurationSource = configurationSource; this.definition = definition; this.extension = extension; + this.beanName = Lazy.of(() -> configurationSource.generateBeanName(definition)); } public String getBeanId() { @@ -90,8 +93,7 @@ public String getImplementationClassName() { } public String getImplementationBeanName() { - return configurationSource.generateBeanName(definition) - + configurationSource.getRepositoryImplementationPostfix().orElse("Impl"); + return beanName.get() + configurationSource.getRepositoryImplementationPostfix().orElse("Impl"); } @Nullable @@ -117,6 +119,11 @@ public String getRepositoryFactoryBeanClassName() { .orElseGet(extension::getRepositoryFactoryBeanClassName); } + @Override + public String getRepositoryBeanName() { + return beanName.get(); + } + @Override public boolean isLazyInit() { return definition.isLazyInit() || !configurationSource.getBootstrapMode().equals(BootstrapMode.DEFAULT); diff --git a/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java index f95323b946..3063a53a84 100644 --- a/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java @@ -90,7 +90,8 @@ default ImplementationLookupConfiguration forFragment(String fragmentInterfaceNa Assert.hasText(fragmentInterfaceName, "Fragment interface name must not be null or empty"); - return new DefaultImplementationLookupConfiguration(this, fragmentInterfaceName); + return new DefaultImplementationLookupConfiguration(this, fragmentInterfaceName, + Introspector.decapitalize(ClassUtils.getShortName(fragmentInterfaceName).concat(getImplementationPostfix()))); } /** @@ -103,7 +104,8 @@ default ImplementationLookupConfiguration forRepositoryConfiguration(RepositoryC Assert.notNull(config, "RepositoryConfiguration must not be null"); - return new DefaultImplementationLookupConfiguration(this, config.getRepositoryInterface()) { + return new DefaultImplementationLookupConfiguration(this, config.getRepositoryInterface(), + config.getImplementationBeanName()) { @Override public Streamable getBasePackages() { 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 80695add4d..4af5ae4add 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -147,7 +147,7 @@ RepositoryConfigurationAdapter buildMetadata(RepositoryConfiguration confi List repositoryFragmentConfigurationStream = fragmentMetadata .getFragmentInterfaces(configuration.getRepositoryInterface()) // - .map(it -> detectRepositoryFragmentConfiguration(it, config)) // + .map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) // .flatMap(Optionals::toStream).toList(); if (repositoryFragmentConfigurationStream.isEmpty()) { @@ -183,26 +183,42 @@ private Optional registerCustomImplementation(RepositoryConfiguration ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory); - String beanName = lookup.getImplementationBeanName(); + String configurationBeanName = lookup.getImplementationBeanName(); // Already a bean configured? - if (registry.containsBeanDefinition(beanName)) { - return Optional.of(beanName); + if (registry.containsBeanDefinition(configurationBeanName)) { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Custom repository implementation already registered: %s", configurationBeanName)); + } + + return Optional.of(configurationBeanName); } Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); return beanDefinition.map(it -> { - if (logger.isDebugEnabled()) { - logger.debug("Registering custom repository implementation: " + lookup.getImplementationBeanName() + " " - + it.getBeanClassName()); - } - + String scannedBeanName = configuration.getConfigurationSource().generateBeanName(it); it.setSource(configuration.getSource()); - registry.registerBeanDefinition(beanName, it); - return beanName; + if (registry.containsBeanDefinition(scannedBeanName)) { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Custom repository implementation already registered: %s %s", scannedBeanName, + it.getBeanClassName())); + } + } else { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Registering custom repository implementation: %s %s", scannedBeanName, + it.getBeanClassName())); + } + + registry.registerBeanDefinition(scannedBeanName, it); + } + + return scannedBeanName; }); } @@ -232,19 +248,20 @@ private Stream registerRepositoryFragmentsImple .toImplementationDetectionConfiguration(metadataReaderFactory); return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) // - .map(it -> detectRepositoryFragmentConfiguration(it, config)) // + .map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) // .flatMap(Optionals::toStream) // .peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) // .peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)); } private Optional detectRepositoryFragmentConfiguration(String fragmentInterface, - ImplementationDetectionConfiguration config) { + ImplementationDetectionConfiguration config, RepositoryConfiguration configuration) { ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface); Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); - return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd)); + return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd, + configuration.getConfigurationSource().generateBeanName(bd))); } private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration repositoryConfiguration, @@ -254,16 +271,21 @@ private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Registering repository fragment implementation: %s %s", beanName, + fragmentConfiguration.getClassName())); + } + bd.setSource(repositoryConfiguration.getSource()); registry.registerBeanDefinition(beanName, bd); }); @@ -276,11 +298,16 @@ private void potentiallyRegisterRepositoryFragment(RepositoryConfiguration co // Already a bean configured? if (registry.containsBeanDefinition(beanName)) { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("RepositoryFragment already registered: %s", beanName)); + } + return; } if (logger.isDebugEnabled()) { - logger.debug("Registering repository fragment: " + beanName); + logger.debug(String.format("Registering RepositoryFragment: %s", beanName)); } BeanDefinitionBuilder fragmentBuilder = BeanDefinitionBuilder.rootBeanDefinition(RepositoryFragment.class, diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java index 48bda60916..35d81f0bde 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java @@ -84,6 +84,22 @@ public interface RepositoryConfiguration beanDefinition; + private final String beanName; /** * Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@code className} of the @@ -45,13 +46,7 @@ public final class RepositoryFragmentConfiguration { * @param className must not be {@literal null} or empty. */ public RepositoryFragmentConfiguration(String interfaceName, String className) { - - Assert.hasText(interfaceName, "Interface name must not be null or empty"); - Assert.hasText(className, "Class name must not be null or empty"); - - this.interfaceName = interfaceName; - this.className = className; - this.beanDefinition = Optional.empty(); + this(interfaceName, className, Optional.empty(), generateBeanName(className)); } /** @@ -69,20 +64,40 @@ public RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinit this.interfaceName = interfaceName; this.className = ConfigurationUtils.getRequiredBeanClassName(beanDefinition); this.beanDefinition = Optional.of(beanDefinition); + this.beanName = generateBeanName(); + } + + RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition, String beanName) { + this(interfaceName, ConfigurationUtils.getRequiredBeanClassName(beanDefinition), Optional.of(beanDefinition), + beanName); } - public RepositoryFragmentConfiguration(String interfaceName, String className, - Optional beanDefinition) { + private RepositoryFragmentConfiguration(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.className = className; this.beanDefinition = beanDefinition; + this.beanName = beanName; + } + + private String generateBeanName() { + return generateBeanName(getClassName()); + } + + private static String generateBeanName(String className) { + return Introspector.decapitalize(ClassUtils.getShortName(className)); } /** * @return name of the implementation bean. */ public String getImplementationBeanName() { - return Introspector.decapitalize(ClassUtils.getShortName(getClassName())); + return this.beanName; } /** diff --git a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java index b272123d2f..6e65c5ce68 100644 --- a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java @@ -19,6 +19,9 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.beans.Introspector; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; @@ -36,6 +39,7 @@ * tests {@link CustomRepositoryImplementationDetector} * * @author Jens Schauder + * @author Mark Paluch */ class CustomRepositoryImplementationDetectorUnitTests { @@ -48,7 +52,8 @@ class CustomRepositoryImplementationDetectorUnitTests { CustomRepositoryImplementationDetector detector = new CustomRepositoryImplementationDetector(environment, resourceLoader, configuration); - { + @BeforeEach + void setUp() { when(configuration.forRepositoryConfiguration(any(RepositoryConfiguration.class))).thenCallRealMethod(); when(configuration.getMetadataReaderFactory()).thenReturn(metadataFactory); when(configuration.getBasePackages()).thenReturn(Streamable.of(this.getClass().getPackage().getName())); @@ -58,10 +63,10 @@ class CustomRepositoryImplementationDetectorUnitTests { @Test // DATACMNS-764, DATACMNS-1371 void returnsNullWhenNoImplementationFound() { - var mock = mock(RepositoryConfiguration.class); + RepositoryConfiguration mock = mock(RepositoryConfiguration.class); + when(mock.getImplementationBeanName()).thenReturn("NoImplementationRepositoryImpl"); - var lookup = configuration - .forRepositoryConfiguration(configFor(NoImplementationRepository.class)); + var lookup = configuration.forRepositoryConfiguration(configFor(NoImplementationRepository.class)); var beanDefinition = detector.detectCustomImplementation(lookup); @@ -71,8 +76,7 @@ void returnsNullWhenNoImplementationFound() { @Test // DATACMNS-764, DATACMNS-1371 void returnsBeanDefinitionWhenOneImplementationIsFound() { - var lookup = configuration - .forRepositoryConfiguration(configFor(SingleSampleRepository.class)); + var lookup = configuration.forRepositoryConfiguration(configFor(SingleSampleRepository.class)); var beanDefinition = detector.detectCustomImplementation(lookup); @@ -91,8 +95,7 @@ void returnsBeanDefinitionMatchingByNameWhenMultipleImplementationAreFound() { return className.contains("$First$") ? "canonicalSampleRepositoryTestImpl" : "otherBeanName"; }); - var lookup = configuration - .forRepositoryConfiguration(configFor(CanonicalSampleRepository.class)); + var lookup = configuration.forRepositoryConfiguration(configFor(CanonicalSampleRepository.class)); assertThat(detector.detectCustomImplementation(lookup)) // .hasValueSatisfying( @@ -113,11 +116,13 @@ void throwsExceptionWhenMultipleImplementationAreFound() { }); } - private RepositoryConfiguration configFor(Class type) { + private RepositoryConfiguration configFor(Class type) { RepositoryConfiguration configuration = mock(RepositoryConfiguration.class); when(configuration.getRepositoryInterface()).thenReturn(type.getSimpleName()); + when(configuration.getImplementationBeanName()) + .thenReturn(Introspector.decapitalize(type.getSimpleName()) + "TestImpl"); when(configuration.getImplementationBasePackages()) .thenReturn(Streamable.of(this.getClass().getPackage().getName())); diff --git a/src/test/java/org/springframework/data/repository/config/DefaultImplementationLookupConfigurationUnitTests.java b/src/test/java/org/springframework/data/repository/config/DefaultImplementationLookupConfigurationUnitTests.java index 4da5c73f62..c2eee8dc0e 100644 --- a/src/test/java/org/springframework/data/repository/config/DefaultImplementationLookupConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/DefaultImplementationLookupConfigurationUnitTests.java @@ -18,8 +18,14 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.beans.Introspector; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.util.ClassUtils; + /** * Unit tests for {@link DefaultImplementationLookupConfigurationUnitTests}. * @@ -28,11 +34,17 @@ */ class DefaultImplementationLookupConfigurationUnitTests { - @Test // DATACMNS-1439 - void shouldConsiderBeanNameDecapitalization() { + ImplementationDetectionConfiguration idcMock = mock(ImplementationDetectionConfiguration.class); - var idcMock = mock(ImplementationDetectionConfiguration.class); + @BeforeEach + void setUp() { when(idcMock.getImplementationPostfix()).thenReturn("Impl"); + when(idcMock.forRepositoryConfiguration(any())).thenCallRealMethod(); + when(idcMock.forFragment(any())).thenCallRealMethod(); + } + + @Test // DATACMNS-1439 + void shouldConsiderBeanNameDecapitalization() { assertThat(getImplementationBeanName(idcMock, "com.acme.UDPRepository")).isEqualTo("UDPRepositoryImpl"); assertThat(getImplementationBeanName(idcMock, "com.acme.UdpRepository")).isEqualTo("udpRepositoryImpl"); @@ -41,19 +53,20 @@ void shouldConsiderBeanNameDecapitalization() { @Test // DATACMNS-1754 void shouldUseSimpleClassNameWhenDefiningImplementationNames() { - var idcMock = mock(ImplementationDetectionConfiguration.class); - when(idcMock.getImplementationPostfix()).thenReturn("Impl"); - - var lookupConfiguration = new DefaultImplementationLookupConfiguration(idcMock, - "com.acme.Repositories$NestedRepository"); + var lookupConfiguration = idcMock.forFragment("com.acme.Repositories$NestedRepository"); assertThat(lookupConfiguration.getImplementationBeanName()).isEqualTo("repositories.NestedRepositoryImpl"); assertThat(lookupConfiguration.getImplementationClassName()).isEqualTo("NestedRepositoryImpl"); } private static String getImplementationBeanName(ImplementationDetectionConfiguration idcMock, String interfaceName) { - var configuration = new DefaultImplementationLookupConfiguration(idcMock, - interfaceName); + var source = mock(RepositoryConfigurationSource.class); + when(source.generateBeanName(any())).thenReturn(Introspector.decapitalize(ClassUtils.getShortName(interfaceName))); + + RepositoryConfiguration repoConfig = new DefaultRepositoryConfiguration<>(source, + BeanDefinitionBuilder.rootBeanDefinition(interfaceName).getBeanDefinition(), null); + + var configuration = idcMock.forRepositoryConfiguration(repoConfig); return configuration.getImplementationBeanName(); } } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java index a2a1f73e8e..bfbc6ea5e2 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java @@ -71,7 +71,7 @@ void shouldConsiderNestedRepositoryInterfacesIfEnabled() { provider.setConsiderNestedRepositoryInterfaces(true); var components = provider.findCandidateComponents("org.springframework.data.repository.config"); - var nestedRepositoryClassName = "org.springframework.data.repository.config.RepositoryComponentProviderUnitTests$MyNestedRepository"; + var nestedRepositoryClassName = "org.springframework.data.repository.config.RepositoryComponentProviderUnitTests$MyNestedRepositoryDefinition"; assertThat(components.size()).isGreaterThanOrEqualTo(1); assertThat(components).extracting(BeanDefinition::getBeanClassName).contains(nestedRepositoryClassName); @@ -91,5 +91,5 @@ void exposesBeanDefinitionRegistry() { assertThat(provider.getRegistry()).isEqualTo(registry); } - interface MyNestedRepository extends Repository {} + interface MyNestedRepositoryDefinition extends Repository {} } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java index 32747be852..2c5da8ef5b 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java @@ -19,14 +19,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.aop.framework.Advised; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; @@ -34,8 +35,15 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.repository.config.RepositoryConfigurationDelegate.LazyRepositoryInjectionPointResolver; +import org.springframework.data.repository.config.annotated.MyAnnotatedRepository; +import org.springframework.data.repository.config.annotated.MyAnnotatedRepositoryImpl; +import org.springframework.data.repository.config.annotated.MyFragmentImpl; +import org.springframework.data.repository.config.excluded.MyOtherRepositoryImpl; +import org.springframework.data.repository.config.stereotype.MyStereotypeRepository; +import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; import org.springframework.data.repository.sample.AddressRepository; import org.springframework.data.repository.sample.AddressRepositoryClient; import org.springframework.data.repository.sample.ProductRepository; @@ -44,13 +52,14 @@ * Unit tests for {@link RepositoryConfigurationDelegate}. * * @author Oliver Gierke + * @author Mark Paluch * @soundtrack Richard Spaven - Tribute (Whole Other*) */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class RepositoryConfigurationDelegateUnitTests { - @Mock RepositoryConfigurationExtension extension; + RepositoryConfigurationExtension extension = new DummyConfigurationExtension(); @Test // DATACMNS-892 void registersRepositoryBeanNameAsAttribute() { @@ -68,8 +77,7 @@ void registersRepositoryBeanNameAsAttribute() { var beanDefinition = definition.getBeanDefinition(); - assertThat(beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE).toString()) - .endsWith("Repository"); + assertThat(beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE).toString()).endsWith("Repository"); } } @@ -87,43 +95,121 @@ void registersDeferredRepositoryInitializationListener() { } - private static ListableBeanFactory assertLazyRepositoryBeanSetup(Class configClass) { + @Test // DATACMNS-1832 + void writesRepositoryScanningMetrics() { + + var startup = Mockito.spy(ApplicationStartup.DEFAULT); var environment = new StandardEnvironment(); - var context = new AnnotationConfigApplicationContext(configClass); + var context = new GenericApplicationContext(); + context.setApplicationStartup(startup); - assertThat(context.getDefaultListableBeanFactory().getAutowireCandidateResolver()) - .isInstanceOf(LazyRepositoryInjectionPointResolver.class); + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + new StandardAnnotationMetadata(TestConfig.class, true), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory()); - var client = context.getBean(AddressRepositoryClient.class); - var repository = client.getRepository(); + var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); - assertThat(Advised.class.isInstance(repository)).isTrue(); + delegate.registerRepositoriesIn(context, extension); - var targetSource = Advised.class.cast(repository).getTargetSource(); - assertThat(targetSource).isNotNull(); + Mockito.verify(startup).start("spring.data.repository.scanning"); + } - return context.getDefaultListableBeanFactory(); + @Test // GH-2487 + void considersDefaultBeanNames() { + + var environment = new StandardEnvironment(); + var context = new GenericApplicationContext(); + + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + AnnotationMetadata.introspect(DefaultBeanNamesConfig.class), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory(), new AnnotationBeanNameGenerator()); + + var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); + + delegate.registerRepositoriesIn(context, extension); + + assertThat(context.getBeanFactory().getBeanDefinition("myOtherRepository")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("myOtherRepositoryImpl")).isNotNull(); } - @Test // DATACMNS-1832 - void writesRepositoryScanningMetrics() { + @Test // GH-2487 + void considersAnnotatedBeanNamesFromRepository() { - var startup = Mockito.spy(ApplicationStartup.DEFAULT); + var environment = new StandardEnvironment(); + var context = new GenericApplicationContext(); + + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + AnnotationMetadata.introspect(AnnotatedDerivedBeanNamesConfig.class), EnableRepositories.class, context, + environment, context.getDefaultListableBeanFactory(), new AnnotationBeanNameGenerator()); + + var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); + + delegate.registerRepositoriesIn(context, extension); + + assertThat(context.getBeanFactory().getBeanDefinition("fooRepository")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("fooRepositoryImpl")).isNotNull(); + } + + @Test // GH-2487 + void considersAnnotatedBeanNamesFromAtComponent() { var environment = new StandardEnvironment(); var context = new GenericApplicationContext(); - context.setApplicationStartup(startup); RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( - new StandardAnnotationMetadata(TestConfig.class, true), EnableRepositories.class, context, environment, - context.getDefaultListableBeanFactory()); + AnnotationMetadata.introspect(AnnotatedBeanNamesConfig.class), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory(), new AnnotationBeanNameGenerator()); var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); delegate.registerRepositoriesIn(context, extension); - Mockito.verify(startup).start("spring.data.repository.scanning"); + assertThat(context.getBeanFactory().getBeanDefinition("myAnnotatedRepository")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("anotherBeanName")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("fragment")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("fragmentFragment")).isNotNull(); + } + + @Test // GH-2487 + void skipsRegistrationOnAlreadyRegisteredBeansUsingAtComponentNames() { + + var environment = new StandardEnvironment(); + var context = new GenericApplicationContext(); + context.setAllowBeanDefinitionOverriding(false); + context.registerBean("fragment", MyFragmentImpl.class); + context.registerBean("anotherBeanName", MyAnnotatedRepositoryImpl.class); + + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + AnnotationMetadata.introspect(AnnotatedBeanNamesConfig.class), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory(), new AnnotationBeanNameGenerator()); + + var delegate = new RepositoryConfigurationDelegate(configSource, context, environment); + + delegate.registerRepositoriesIn(context, extension); + + assertThat(context.getBeanFactory().getBeanDefinition("myAnnotatedRepository")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("anotherBeanName")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("fragment")).isNotNull(); + assertThat(context.getBeanFactory().getBeanDefinition("fragmentFragment")).isNotNull(); + } + + private static ListableBeanFactory assertLazyRepositoryBeanSetup(Class configClass) { + + var context = new AnnotationConfigApplicationContext(configClass); + + assertThat(context.getDefaultListableBeanFactory().getAutowireCandidateResolver()) + .isInstanceOf(LazyRepositoryInjectionPointResolver.class); + + var client = context.getBean(AddressRepositoryClient.class); + var repository = client.getRepository(); + + assertThat(Advised.class.isInstance(repository)).isTrue(); + + var targetSource = Advised.class.cast(repository).getTargetSource(); + assertThat(targetSource).isNotNull(); + + return context.getDefaultListableBeanFactory(); } @EnableRepositories(basePackageClasses = ProductRepository.class) @@ -140,4 +226,28 @@ static class LazyConfig {} includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AddressRepository.class), bootstrapMode = BootstrapMode.DEFERRED) static class DeferredConfig {} + + @EnableRepositories(basePackageClasses = MyOtherRepository.class, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyOtherRepository.class), + excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyOtherRepositoryImpl.class)) + static class DefaultBeanNamesConfig {} + + @EnableRepositories(basePackageClasses = MyStereotypeRepository.class, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyStereotypeRepository.class)) + static class AnnotatedDerivedBeanNamesConfig {} + + @EnableRepositories(basePackageClasses = MyAnnotatedRepository.class) + static class AnnotatedBeanNamesConfig {} + + static class DummyConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + public String getRepositoryFactoryBeanClassName() { + return DummyRepositoryFactoryBean.class.getName(); + } + + @Override + protected String getModulePrefix() { + return "commons"; + } + } } diff --git a/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepository.java b/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepository.java new file mode 100644 index 0000000000..973743628c --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.annotated; + +import org.springframework.data.mapping.Person; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Mark Paluch + */ +public interface MyAnnotatedRepository extends CrudRepository, MyFragment {} diff --git a/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepositoryImpl.java b/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepositoryImpl.java new file mode 100644 index 0000000000..d424ecc3f8 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/annotated/MyAnnotatedRepositoryImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.annotated; + +import org.springframework.stereotype.Component; + +/** + * @author Mark Paluch + */ +@Component("anotherBeanName") +public class MyAnnotatedRepositoryImpl {} diff --git a/src/test/java/org/springframework/data/repository/config/annotated/MyFragment.java b/src/test/java/org/springframework/data/repository/config/annotated/MyFragment.java new file mode 100644 index 0000000000..87be1eccc3 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/annotated/MyFragment.java @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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.annotated; + +/** + * @author Mark Paluch + */ +interface MyFragment {} diff --git a/src/test/java/org/springframework/data/repository/config/annotated/MyFragmentImpl.java b/src/test/java/org/springframework/data/repository/config/annotated/MyFragmentImpl.java new file mode 100644 index 0000000000..2493dd9e33 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/annotated/MyFragmentImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.annotated; + +import org.springframework.stereotype.Component; + +/** + * @author Mark Paluch + */ +@Component("fragment") +public class MyFragmentImpl implements MyFragment {} diff --git a/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepository.java b/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepository.java new file mode 100644 index 0000000000..d5fb567711 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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.stereotype; + +import org.springframework.data.mapping.Person; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; + +/** + * @author Mark Paluch + */ +@Component("fooRepository") +public interface MyStereotypeRepository extends CrudRepository {} diff --git a/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepositoryImpl.java b/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepositoryImpl.java new file mode 100644 index 0000000000..9e5321fdbf --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/stereotype/MyStereotypeRepositoryImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.stereotype; + +import org.springframework.stereotype.Component; + +/** + * @author Mark Paluch + */ +@Component("fooRepositoryImpl") +public class MyStereotypeRepositoryImpl {}