Consider component names for custom implementation and fragment bean registration.

We now consider the the actual repository & fragment bean name when checking for existing bean definitions of default custom implementation beans. Previously, we used the repository interface name without considering the repository bean name.

Closes #2487.
Original Pull Request: #2488
This commit is contained in:
Mark Paluch
2021-10-20 14:00:42 +02:00
committed by Christoph Strobl
parent 103d41f7f4
commit 70cda7949d
18 changed files with 446 additions and 90 deletions

View File

@@ -19,6 +19,9 @@ import static org.assertj.core.api.Assertions.*;
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 @@ import org.springframework.mock.env.MockEnvironment;
* 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 @@ class CustomRepositoryImplementationDetectorUnitTests {
@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 @@ class CustomRepositoryImplementationDetectorUnitTests {
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 @@ class CustomRepositoryImplementationDetectorUnitTests {
});
}
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()));

View File

@@ -18,8 +18,14 @@ package org.springframework.data.repository.config;
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,12 +34,18 @@ import org.junit.jupiter.api.Test;
*/
class DefaultImplementationLookupConfigurationUnitTests {
ImplementationDetectionConfiguration 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() {
var idcMock = mock(ImplementationDetectionConfiguration.class);
when(idcMock.getImplementationPostfix()).thenReturn("Impl");
assertThat(getImplementationBeanName(idcMock, "com.acme.UDPRepository")).isEqualTo("UDPRepositoryImpl");
assertThat(getImplementationBeanName(idcMock, "com.acme.UdpRepository")).isEqualTo("udpRepositoryImpl");
}
@@ -41,19 +53,20 @@ class DefaultImplementationLookupConfigurationUnitTests {
@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();
}
}

View File

@@ -71,7 +71,7 @@ class RepositoryComponentProviderUnitTests {
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 @@ class RepositoryComponentProviderUnitTests {
assertThat(provider.getRegistry()).isEqualTo(registry);
}
interface MyNestedRepository extends Repository<Person, Long> {}
interface MyNestedRepositoryDefinition extends Repository<Person, Long> {}
}

View File

@@ -19,14 +19,15 @@ import static org.assertj.core.api.Assertions.*;
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.annotation.FilterType;
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 @@ import org.springframework.data.repository.sample.ProductRepository;
* 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 @@ class RepositoryConfigurationDelegateUnitTests {
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,25 +95,6 @@ class RepositoryConfigurationDelegateUnitTests {
}
private static ListableBeanFactory assertLazyRepositoryBeanSetup(Class<?> configClass) {
var environment = new StandardEnvironment();
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();
}
@Test // DATACMNS-1832
void writesRepositoryScanningMetrics() {
@@ -126,6 +115,103 @@ class RepositoryConfigurationDelegateUnitTests {
Mockito.verify(startup).start("spring.data.repository.scanning");
}
@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 // GH-2487
void considersAnnotatedBeanNamesFromRepository() {
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();
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();
}
@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)
static class TestConfig {}
@@ -140,4 +226,28 @@ class RepositoryConfigurationDelegateUnitTests {
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";
}
}
}

View File

@@ -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<Person, String>, MyFragment {}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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<Person, String> {}

View File

@@ -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 {}