Skip to content

Commit

Permalink
DATACMNS-1371 - Improvements to custom implementation scanning.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Aug 14, 2018
1 parent 3f613ff commit 13b1150
Show file tree
Hide file tree
Showing 18 changed files with 734 additions and 351 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,26 @@
*/
package org.springframework.data.repository.cdi;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import java.util.stream.Stream;

import javax.enterprise.inject.CreationException;
import javax.enterprise.inject.UnsatisfiedResolutionException;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.FragmentMetadata;
import org.springframework.data.repository.config.ImplementationDetectionConfiguration;
import org.springframework.data.repository.config.ImplementationLookupConfiguration;
import org.springframework.data.repository.config.RepositoryFragmentConfiguration;
import org.springframework.data.repository.config.RepositoryFragmentDiscovery;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
Expand All @@ -61,6 +54,7 @@ public class CdiRepositoryContext {
private final ClassLoader classLoader;
private final CustomRepositoryImplementationDetector detector;
private final MetadataReaderFactory metadataReaderFactory;
private final FragmentMetadata metdata;

/**
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize
Expand All @@ -69,16 +63,8 @@ public class CdiRepositoryContext {
* @param classLoader must not be {@literal null}.
*/
public CdiRepositoryContext(ClassLoader classLoader) {

Assert.notNull(classLoader, "ClassLoader must not be null!");

this.classLoader = classLoader;

Environment environment = new StandardEnvironment();
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader);

this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.detector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader);
this(classLoader, new CustomRepositoryImplementationDetector(new StandardEnvironment(),
new PathMatchingResourcePatternResolver(classLoader)));
}

/**
Expand All @@ -97,6 +83,7 @@ public CdiRepositoryContext(ClassLoader classLoader, CustomRepositoryImplementat

this.classLoader = classLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.metdata = new FragmentMetadata(metadataReaderFactory);
this.detector = detector;
}

Expand Down Expand Up @@ -130,14 +117,11 @@ Class<?> loadClass(String className) {
Stream<RepositoryFragmentConfiguration> getRepositoryFragments(CdiRepositoryConfiguration configuration,
Class<?> repositoryInterface) {

ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName());

RepositoryFragmentDiscovery fragmentConfiguration = new CdiRepositoryFragmentDiscovery(configuration);
CdiImplementationDetectionConfiguration config = new CdiImplementationDetectionConfiguration(configuration,
metadataReaderFactory);

return Arrays.stream(classMetadata.getInterfaceNames()) //
.filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) //
.map(it -> FragmentMetadata.of(it, fragmentConfiguration)) //
.map(this::detectRepositoryFragmentConfiguration) //
return metdata.getFragmentInterfaces(repositoryInterface.getName()) //
.map(it -> detectRepositoryFragmentConfiguration(it, config)) //
.flatMap(Optionals::toStream);
}

Expand All @@ -152,26 +136,22 @@ Stream<RepositoryFragmentConfiguration> getRepositoryFragments(CdiRepositoryConf
Optional<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {

String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
ImplementationDetectionConfiguration configuration = new CdiImplementationDetectionConfiguration(
cdiRepositoryConfiguration, metadataReaderFactory);
ImplementationLookupConfiguration lookup = configuration.forFragment(repositoryType.getName());

Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
className, //
className, Collections.singleton(repositoryType.getPackage().getName()), //
Collections.emptySet(), //
BeanDefinition::getBeanClassName);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);

return beanDefinition.map(this::loadBeanClass);
}

private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
FragmentMetadata configuration) {
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(String fragmentInterfaceName,
CdiImplementationDetectionConfiguration config) {

String className = configuration.getFragmentImplementationClassName();
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterfaceName);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);

Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(className, null,
configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName);

return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterfaceName, bd));
}

@Nullable
Expand All @@ -182,45 +162,37 @@ private Class<?> loadBeanClass(AbstractBeanDefinition definition) {
return beanClassName == null ? null : loadClass(beanClassName);
}

private static ClassMetadata getClassMetadata(MetadataReaderFactory metadataReaderFactory, String className) {

try {
return metadataReaderFactory.getMetadataReader(className).getClassMetadata();
} catch (IOException e) {
throw new CreationException(String.format("Cannot parse %s metadata.", className), e);
}
}

private static String getCustomImplementationClassName(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {

String configuredPostfix = cdiRepositoryConfiguration.getRepositoryImplementationPostfix();
Assert.hasText(configuredPostfix, "Configured repository postfix must not be null or empty!");

return ClassUtils.getShortName(repositoryType) + configuredPostfix;
}

@RequiredArgsConstructor
private static class CdiRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery {
private static class CdiImplementationDetectionConfiguration implements ImplementationDetectionConfiguration {

private final CdiRepositoryConfiguration configuration;
private final @Getter MetadataReaderFactory metadataReaderFactory;

/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class));
public String getImplementationPostfix() {
return configuration.getRepositoryImplementationPostfix();
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages()
*/
@Override
public Optional<String> getRepositoryImplementationPostfix() {
return Optional.of(configuration.getRepositoryImplementationPostfix());
public Streamable<String> getBasePackages() {
return Streamable.empty();
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return Streamable.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,27 @@
*/
package org.springframework.data.repository.config;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;

/**
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository}
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If
* configured with a {@link ImplementationDetectionConfiguration} at construction time, the necessary component scan is
* executed on first access, cached and its result is the filtered on every further implementation lookup according to
* the given {@link ImplementationDetectionConfiguration}. If none is given initially, every invocation to
* {@link #detectCustomImplementation(String, String, ImplementationDetectionConfiguration)} will issue a new component
* scan.
*
* @author Oliver Gierke
* @author Mark Paluch
Expand All @@ -46,76 +44,91 @@
* @author Jens Schauder
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class CustomRepositoryImplementationDetector {

private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/%s.class";
private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/*%s.class";
private static final String AMBIGUOUS_CUSTOM_IMPLEMENTATIONS = "Ambiguous custom implementations detected! Found %s but expected a single implementation!";

private final @NonNull MetadataReaderFactory metadataReaderFactory;
private final @NonNull Environment environment;
private final @NonNull ResourceLoader resourceLoader;
private final Environment environment;
private final ResourceLoader resourceLoader;
private final Lazy<Set<BeanDefinition>> implementationCandidates;

/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param configuration the {@link RepositoryConfiguration} to consider.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
* Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment},
* {@link ResourceLoader} and {@link ImplementationDetectionConfiguration}. The latter will be registered for a
* one-time component scan for implementation candidates that will the be used and filtered in all subsequent calls to
* {@link #detectCustomImplementation(RepositoryConfiguration)}.
*
* @param environment must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader,
ImplementationDetectionConfiguration configuration) {

Assert.notNull(environment, "Environment must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
Assert.notNull(configuration, "ImplementationDetectionConfiguration must not be null!");

this.environment = environment;
this.resourceLoader = resourceLoader;
this.implementationCandidates = Lazy.of(() -> findCandidateBeanDefinitions(configuration));
}

/**
* Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment} and
* {@link ResourceLoader}. Calls to {@link #detectCustomImplementation(ImplementationLookupConfiguration)} will issue
* scans for
*
* @param environment must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
*/
@SuppressWarnings("deprecation")
public Optional<AbstractBeanDefinition> detectCustomImplementation(RepositoryConfiguration<?> configuration) {
public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader) {

// TODO 2.0: Extract into dedicated interface for custom implementation lookup configuration.
Assert.notNull(environment, "Environment must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");

return detectCustomImplementation( //
configuration.getImplementationClassName(), //
configuration.getImplementationBeanName(), //
configuration.getImplementationBasePackages(), //
configuration.getExcludeFilters(), //
bd -> configuration.getConfigurationSource().generateBeanName(bd));
this.environment = environment;
this.resourceLoader = resourceLoader;
this.implementationCandidates = Lazy.empty();
}

/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param className must not be {@literal null}.
* @param beanName may be {@literal null}
* @param basePackages must not be {@literal null}.
* @param excludeFilters must not be {@literal null}.
* @param beanNameGenerator must not be {@literal null}.
* @param lookup must not be {@literal null}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
*/
public Optional<AbstractBeanDefinition> detectCustomImplementation(String className, @Nullable String beanName,
Iterable<String> basePackages, Iterable<TypeFilter> excludeFilters,
Function<BeanDefinition, String> beanNameGenerator) {
public Optional<AbstractBeanDefinition> detectCustomImplementation(ImplementationLookupConfiguration lookup) {

Assert.notNull(className, "ClassName must not be null!");
Assert.notNull(basePackages, "BasePackages must not be null!");
Assert.notNull(lookup, "ImplementationLookupConfiguration must not be null!");

Set<BeanDefinition> definitions = findCandidateBeanDefinitions(className, basePackages, excludeFilters);
Set<BeanDefinition> definitions = implementationCandidates.getOptional()
.orElseGet(() -> findCandidateBeanDefinitions(lookup)).stream() //
.filter(lookup::matches) //
.collect(StreamUtils.toUnmodifiableSet());

return SelectionSet //
.of(definitions, c -> c.isEmpty() ? Optional.empty() : throwAmbiguousCustomImplementationException(c)) //
.filterIfNecessary(bd -> beanName != null && beanName.equals(beanNameGenerator.apply(bd)))//
.uniqueResult().map(it -> AbstractBeanDefinition.class.cast(it));
.filterIfNecessary(lookup::hasMatchingBeanName) //
.uniqueResult() //
.map(AbstractBeanDefinition.class::cast);
}

Set<BeanDefinition> findCandidateBeanDefinitions(String className, Iterable<String> basePackages,
Iterable<TypeFilter> excludeFilters) {
private Set<BeanDefinition> findCandidateBeanDefinitions(ImplementationDetectionConfiguration config) {

// Build pattern to lookup implementation class
String postfix = config.getImplementationPostfix();

// Build classpath scanner and lookup bean definition
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false,
environment);
provider.setResourceLoader(resourceLoader);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, className));
provider.setMetadataReaderFactory(metadataReaderFactory);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, postfix));
provider.setMetadataReaderFactory(config.getMetadataReaderFactory());
provider.addIncludeFilter((reader, factory) -> true);

excludeFilters.forEach(it -> provider.addExcludeFilter(it));
config.getExcludeFilters().forEach(it -> provider.addExcludeFilter(it));

return Streamable.of(basePackages).stream()//
return config.getBasePackages().stream()//
.flatMap(it -> provider.findCandidateComponents(it).stream())//
.collect(Collectors.toSet());
}
Expand Down
Loading

0 comments on commit 13b1150

Please sign in to comment.