DATACMNS-1371 - Improvements to custom implementation scanning.

CustomRepositoryImplementationDetector now works in two differend modes. If initialized with an ImplementationDetectionConfiguration, it will trigger a canonical, cached component scan for implementation types matching the configured name pattern. Individual custom implementation lookups will then select from this initially scanned set of bean definitions to pick the matching implementation class and potentially resolve ambiguities.
This commit is contained in:
Oliver Gierke
2018-08-09 18:00:56 +02:00
parent 3f613ff13f
commit 13b115068d
18 changed files with 746 additions and 363 deletions

View File

@@ -16,7 +16,6 @@
package org.springframework.data.repository.cdi;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.Serializable;
@@ -25,7 +24,6 @@ import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.spi.CreationalContext;
@@ -35,11 +33,12 @@ import javax.inject.Named;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.ImplementationLookupConfiguration;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
@@ -153,13 +152,15 @@ public class CdiRepositoryBeanUnitTests {
bean.create(mock(CreationalContext.class), SampleRepository.class);
verify(detector).detectCustomImplementation( //
eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), //
eq("CdiRepositoryBeanUnitTests.SampleRepositoryImpl"), //
anySet(), //
anySet(), //
Mockito.any(Function.class) //
);
ArgumentCaptor<ImplementationLookupConfiguration> captor = ArgumentCaptor
.forClass(ImplementationLookupConfiguration.class);
verify(detector).detectCustomImplementation(captor.capture());
ImplementationLookupConfiguration configuration = captor.getValue();
assertThat(configuration.getImplementationBeanName()).isEqualTo("cdiRepositoryBeanUnitTests.SampleRepositoryImpl");
assertThat(configuration.getImplementationClassName()).isEqualTo("CdiRepositoryBeanUnitTests.SampleRepositoryImpl");
}
@Test // DATACMNS-1233

View File

@@ -15,18 +15,14 @@
*/
package org.springframework.data.repository.config;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.HashSet;
import java.util.Optional;
import java.util.function.Function;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.mockito.Answers;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.env.Environment;
@@ -34,7 +30,8 @@ import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetectorUnitTests.First.CanonicalSampleRepositoryTestImpl;
import org.springframework.data.util.Streamable;
import org.springframework.mock.env.MockEnvironment;
/**
@@ -47,70 +44,107 @@ public class CustomRepositoryImplementationDetectorUnitTests {
MetadataReaderFactory metadataFactory = new SimpleMetadataReaderFactory();
Environment environment = new MockEnvironment();
ResourceLoader resourceLoader = new DefaultResourceLoader();
Function<BeanDefinition, String> nameGenerator = mock(Function.class);
ImplementationDetectionConfiguration configuration = mock(ImplementationDetectionConfiguration.class,
Answers.RETURNS_MOCKS);
CustomRepositoryImplementationDetector detector = spy(
new CustomRepositoryImplementationDetector(metadataFactory, environment, resourceLoader));
CustomRepositoryImplementationDetector detector = new CustomRepositoryImplementationDetector(environment,
resourceLoader, configuration);
{
doReturn("notTheBeanYouAreLookingFor").when(nameGenerator).apply(any(BeanDefinition.class));
when(configuration.forRepositoryConfiguration(any(RepositoryConfiguration.class))).thenCallRealMethod();
when(configuration.getMetadataReaderFactory()).thenReturn(metadataFactory);
when(configuration.getBasePackages()).thenReturn(Streamable.of(this.getClass().getPackage().getName()));
when(configuration.getImplementationPostfix()).thenReturn("TestImpl");
}
@Test // DATACMNS-764
@Test // DATACMNS-764, DATACMNS-1371
public void returnsNullWhenNoImplementationFound() {
doReturn(emptySet()).when(detector).findCandidateBeanDefinitions(anyString(), anyListOf(String.class),
anyListOf(TypeFilter.class));
RepositoryConfiguration mock = mock(RepositoryConfiguration.class);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation("className", "beanName", emptyList(),
emptyList(), nameGenerator);
ImplementationLookupConfiguration lookup = configuration
.forRepositoryConfiguration(configFor(NoImplementationRepository.class));
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);
assertThat(beanDefinition).isEmpty();
}
@Test // DATACMNS-764
@Test // DATACMNS-764, DATACMNS-1371
public void returnsBeanDefinitionWhenOneImplementationIsFound() {
AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class);
ImplementationLookupConfiguration lookup = configuration
.forRepositoryConfiguration(configFor(SingleSampleRepository.class));
doReturn(new HashSet<>(singleton(expectedBeanDefinition))).when(detector).findCandidateBeanDefinitions(anyString(),
anyListOf(String.class), anyListOf(TypeFilter.class));
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation("className", "beanName", emptyList(),
emptyList(), nameGenerator);
assertThat(beanDefinition).contains(expectedBeanDefinition);
assertThat(beanDefinition).hasValueSatisfying(
it -> assertThat(it.getBeanClassName()).isEqualTo(SingleSampleRepositoryTestImpl.class.getName()));
}
@Test // DATACMNS-764
@Test // DATACMNS-764, DATACMNS-1371
public void returnsBeanDefinitionMatchingByNameWhenMultipleImplementationAreFound() {
AbstractBeanDefinition wrongBeanDefinition = mock(AbstractBeanDefinition.class);
AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class);
when(configuration.generateBeanName(any())).then(it -> {
doReturn("expected").when(nameGenerator).apply(expectedBeanDefinition);
BeanDefinition definition = it.getArgument(0);
String className = definition.getBeanClassName();
doReturn(new HashSet<>(asList(wrongBeanDefinition, expectedBeanDefinition))).when(detector)
.findCandidateBeanDefinitions(anyString(), anyListOf(String.class), anyListOf(TypeFilter.class));
return className.contains("$First$") ? "canonicalSampleRepositoryTestImpl" : "otherBeanName";
});
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation("className", "expected", emptyList(),
emptyList(), nameGenerator);
ImplementationLookupConfiguration lookup = configuration
.forRepositoryConfiguration(configFor(CanonicalSampleRepository.class));
assertThat( beanDefinition).contains(expectedBeanDefinition);
assertThat(detector.detectCustomImplementation(lookup)) //
.hasValueSatisfying(
it -> assertThat(it.getBeanClassName()).isEqualTo(CanonicalSampleRepositoryTestImpl.class.getName()));
}
@Test(expected = IllegalStateException.class) // DATACMNS-764
@Test // DATACMNS-764, DATACMNS-1371
public void throwsExceptionWhenMultipleImplementationAreFound() {
AbstractBeanDefinition wrongBeanDefinition = mock(AbstractBeanDefinition.class);
AbstractBeanDefinition expectedBeanDefinition = mock(AbstractBeanDefinition.class);
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
doReturn("expected").when(nameGenerator).apply(any(BeanDefinition.class));
ImplementationLookupConfiguration lookup = mock(ImplementationLookupConfiguration.class);
doReturn(new HashSet<>(asList(wrongBeanDefinition, expectedBeanDefinition))).when(detector)
.findCandidateBeanDefinitions(anyString(), anyListOf(String.class), anyListOf(TypeFilter.class));
when(lookup.hasMatchingBeanName(any())).thenReturn(true);
when(lookup.matches(any())).thenReturn(true);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation("className", "expected", emptyList(),
emptyList(), nameGenerator);
detector.detectCustomImplementation(lookup);
});
}
private RepositoryConfiguration configFor(Class<?> type) {
RepositoryConfiguration<?> configuration = mock(RepositoryConfiguration.class);
when(configuration.getRepositoryInterface()).thenReturn(type.getSimpleName());
when(configuration.getImplementationBasePackages())
.thenReturn(Streamable.of(this.getClass().getPackage().getName()));
return configuration;
}
// No implementation
interface NoImplementationRepository {}
// Single implementation
interface SingleSampleRepository {}
static class SingleSampleRepositoryTestImpl implements SingleSampleRepository {}
// Multiple implementations
interface CanonicalSampleRepository {}
static class First {
static class CanonicalSampleRepositoryTestImpl implements CanonicalSampleRepository {}
}
static class Second {
static class CanonicalSampleRepositoryTestImpl implements CanonicalSampleRepository {}
}
}

View File

@@ -23,14 +23,10 @@ import lombok.Value;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
@@ -49,22 +45,12 @@ public class DefaultRepositoryConfigurationUnitTests {
RepositoryConfigurationExtension extension = new SimplerRepositoryConfigurationExtension("factory", "module");
@Before
public void before() {
RepositoryBeanNameGenerator generator = new RepositoryBeanNameGenerator(getClass().getClassLoader());
Answer<Object> answer = invocation -> generator.generateBeanName((BeanDefinition) invocation.getArguments()[0]);
when(source.generateBeanName(Mockito.any(BeanDefinition.class))).then(answer);
}
@Test
public void supportsBasicConfiguration() {
RepositoryConfiguration<RepositoryConfigurationSource> configuration = getConfiguration(source);
assertThat(configuration.getConfigurationSource()).isEqualTo(source);
assertThat(configuration.getImplementationBeanName()).isEqualTo("myRepositoryImpl");
assertThat(configuration.getImplementationClassName()).isEqualTo("MyRepositoryImpl");
assertThat(configuration.getRepositoryInterface()).isEqualTo("com.acme.MyRepository");
assertThat(configuration.getQueryLookupStrategyKey()).isEqualTo(Key.CREATE_IF_NOT_FOUND);
assertThat(configuration.isLazyInit()).isFalse();