From 13b115068d171f25e6e553ebfd55eec54120db5f Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 9 Aug 2018 18:00:56 +0200 Subject: [PATCH] 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. --- .../repository/cdi/CdiRepositoryContext.java | 112 +++++------- ...ustomRepositoryImplementationDetector.java | 111 +++++++----- ...aultImplementationLookupConfiguration.java | 170 ++++++++++++++++++ .../DefaultRepositoryConfiguration.java | 26 +++ .../repository/config/FragmentMetadata.java | 85 ++++----- .../ImplementationDetectionConfiguration.java | 116 ++++++++++++ .../ImplementationLookupConfiguration.java | 59 ++++++ .../RepositoryBeanDefinitionBuilder.java | 82 +++------ .../config/RepositoryConfiguration.java | 37 ++-- .../RepositoryConfigurationDelegate.java | 4 +- .../config/RepositoryConfigurationSource.java | 11 ++ .../RepositoryConfigurationSourceSupport.java | 57 ++++++ .../config/RepositoryFragmentDiscovery.java | 45 ----- .../org/springframework/data/util/Lazy.java | 16 ++ .../springframework/data/util/Streamable.java | 25 ++- .../cdi/CdiRepositoryBeanUnitTests.java | 21 +-- ...sitoryImplementationDetectorUnitTests.java | 118 +++++++----- ...faultRepositoryConfigurationUnitTests.java | 14 -- 18 files changed, 746 insertions(+), 363 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java create mode 100644 src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java create mode 100644 src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java delete mode 100644 src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java diff --git a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java index 2969cbc12..8bddf7dfd 100644 --- a/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/cdi/CdiRepositoryContext.java @@ -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; @@ -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 @@ -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))); } /** @@ -97,6 +83,7 @@ public class CdiRepositoryContext { this.classLoader = classLoader; this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.metdata = new FragmentMetadata(metadataReaderFactory); this.detector = detector; } @@ -130,14 +117,11 @@ public class CdiRepositoryContext { Stream getRepositoryFragments(CdiRepositoryConfiguration configuration, Class repositoryInterface) { - ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName()); + CdiImplementationDetectionConfiguration config = new CdiImplementationDetectionConfiguration(configuration, + metadataReaderFactory); - RepositoryFragmentDiscovery fragmentConfiguration = new CdiRepositoryFragmentDiscovery(configuration); - - 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); } @@ -152,26 +136,22 @@ public class CdiRepositoryContext { Optional> getCustomImplementationClass(Class repositoryType, CdiRepositoryConfiguration cdiRepositoryConfiguration) { - String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration); + ImplementationDetectionConfiguration configuration = new CdiImplementationDetectionConfiguration( + cdiRepositoryConfiguration, metadataReaderFactory); + ImplementationLookupConfiguration lookup = configuration.forFragment(repositoryType.getName()); - Optional beanDefinition = detector.detectCustomImplementation( // - className, // - className, Collections.singleton(repositoryType.getPackage().getName()), // - Collections.emptySet(), // - BeanDefinition::getBeanClassName); + Optional beanDefinition = detector.detectCustomImplementation(lookup); return beanDefinition.map(this::loadBeanClass); } - private Optional detectRepositoryFragmentConfiguration( - FragmentMetadata configuration) { + private Optional detectRepositoryFragmentConfiguration(String fragmentInterfaceName, + CdiImplementationDetectionConfiguration config) { - String className = configuration.getFragmentImplementationClassName(); + ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterfaceName); + Optional beanDefinition = detector.detectCustomImplementation(lookup); - Optional 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 @@ -182,45 +162,37 @@ public class CdiRepositoryContext { 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 String getImplementationPostfix() { + return configuration.getRepositoryImplementationPostfix(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return Streamable.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters() */ @Override public Streamable getExcludeFilters() { - return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix() - */ - @Override - public Optional getRepositoryImplementationPostfix() { - return Optional.of(configuration.getRepositoryImplementationPostfix()); + return Streamable.empty(); } } } 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 f364ca285..d30b925b2 100644 --- a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java +++ b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java @@ -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 @@ -46,76 +44,91 @@ import org.springframework.util.Assert; * @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> 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}. */ - @SuppressWarnings("deprecation") - public Optional detectCustomImplementation(RepositoryConfiguration configuration) { + public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader, + ImplementationDetectionConfiguration configuration) { - // 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!"); + Assert.notNull(configuration, "ImplementationDetectionConfiguration 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.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}. + */ + public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader) { + + Assert.notNull(environment, "Environment must not be null!"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); + + 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 detectCustomImplementation(String className, @Nullable String beanName, - Iterable basePackages, Iterable excludeFilters, - Function beanNameGenerator) { + public Optional 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 definitions = findCandidateBeanDefinitions(className, basePackages, excludeFilters); + Set 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 findCandidateBeanDefinitions(String className, Iterable basePackages, - Iterable excludeFilters) { + private Set 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()); } diff --git a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java new file mode 100644 index 000000000..372715328 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java @@ -0,0 +1,170 @@ +/* + * Copyright 2018 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 + * + * http://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; + +import java.io.IOException; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReader; +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.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of {@link ImplementationLookupConfiguration}. + * + * @author Oliver Gierke + * @since 2.1 + */ +class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration { + + private final ImplementationDetectionConfiguration config; + 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 implementationClassName must not be {@literal null} or empty. + */ + DefaultImplementationLookupConfiguration(ImplementationDetectionConfiguration config, String interfaceName) { + + Assert.notNull(config, "ImplementationDetectionConfiguration must not be null!"); + Assert.hasText(interfaceName, "Interface name must not be null or empty!"); + + this.config = config; + this.interfaceName = interfaceName; + this.beanName = StringUtils + .uncapitalize(ClassUtils.getShortName(interfaceName).concat(config.getImplementationPostfix())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationBeanName() + */ + @Override + public String getImplementationBeanName() { + return beanName; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getImplementationPostfix() + */ + @Override + public String getImplementationPostfix() { + return config.getImplementationPostfix(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getExcludeFilters() + */ + @Override + public Streamable getExcludeFilters() { + return config.getExcludeFilters().and(Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getMetadataReaderFactory() + */ + @Override + public MetadataReaderFactory getMetadataReaderFactory() { + return config.getMetadataReaderFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return Streamable.of(ClassUtils.getPackageName(interfaceName)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationClassName() + */ + @Override + public String getImplementationClassName() { + return ClassUtils.getShortName(interfaceName).concat(getImplementationPostfix()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#hasMatchingBeanName(org.springframework.beans.factory.config.BeanDefinition) + */ + @Override + public boolean hasMatchingBeanName(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + return beanName != null && beanName.equals(config.generateBeanName(definition)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.ImplementationLookupConfiguration#matches(org.springframework.beans.factory.config.BeanDefinition, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean matches(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + String beanClassName = definition.getBeanClassName(); + + if (beanClassName == null || isExcluded(beanClassName, getExcludeFilters())) { + return false; + } + + String beanPackage = ClassUtils.getPackageName(beanClassName); + String shortName = ClassUtils.getShortName(beanClassName); + String localName = shortName.substring(shortName.lastIndexOf('.') + 1); + + return localName.equals(getImplementationClassName()) // + && getBasePackages().stream().anyMatch(it -> beanPackage.startsWith(it)); + } + + private boolean isExcluded(String beanClassName, Streamable filters) { + + try { + + MetadataReader reader = getMetadataReaderFactory().getMetadataReader(beanClassName); + return filters.stream().anyMatch(it -> matches(it, reader)); + + } catch (IOException o_O) { + return true; + } + } + + private boolean matches(TypeFilter filter, MetadataReader reader) { + + try { + return filter.match(reader, getMetadataReaderFactory()); + } catch (IOException e) { + return false; + } + } +} 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 e69ca1789..2c9995515 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -21,11 +21,13 @@ import lombok.RequiredArgsConstructor; import java.util.Optional; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReaderFactory; 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.Streamable; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -179,4 +181,28 @@ public class DefaultRepositoryConfiguration getExcludeFilters() { return configurationSource.getExcludeFilters(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfiguration#toImplementationDetectionConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + + Assert.notNull(factory, "MetadataReaderFactory must not be null!"); + + return configurationSource.toImplementationDetectionConfiguration(factory); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfiguration#toLookupConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { + + Assert.notNull(factory, "MetadataReaderFactory must not be null!"); + + return toImplementationDetectionConfiguration(factory).forRepositoryConfiguration(this); + } } diff --git a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java index a71a48487..f83a48041 100644 --- a/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/config/FragmentMetadata.java @@ -15,34 +15,44 @@ */ package org.springframework.data.repository.config; -import lombok.Value; +import lombok.RequiredArgsConstructor; import java.io.IOException; -import java.util.Collections; -import java.util.List; +import java.util.Arrays; import java.util.stream.Stream; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; 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.util.StreamUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Value object for a discovered Repository fragment interface. * * @author Mark Paluch + * @author Oliver Gierke * @since 2.1 */ -@Value(staticConstructor = "of") +@RequiredArgsConstructor public class FragmentMetadata { - private String fragmentInterfaceName; - private RepositoryFragmentDiscovery configuration; + private final MetadataReaderFactory factory; + + /** + * Returns all interfaces to be considered fragment ones for the given source interface. + * + * @param interfaceName must not be {@literal null} or empty. + * @return + */ + public Stream getFragmentInterfaces(String interfaceName) { + + Assert.hasText(interfaceName, "Interface name must not be null or empty!"); + + return Arrays.stream(getClassMetadata(interfaceName).getInterfaceNames()) // + .filter(this::isCandidate); + } /** * Returns whether the given interface is a fragment candidate. @@ -51,55 +61,28 @@ public class FragmentMetadata { * @param factory must not be {@literal null}. * @return */ - public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) { + private boolean isCandidate(String interfaceName) { Assert.hasText(interfaceName, "Interface name must not be null or empty!"); - Assert.notNull(factory, "MetadataReaderFactory must not be null!"); - - AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory); + AnnotationMetadata metadata = getAnnotationMetadata(interfaceName); return !metadata.hasAnnotation(NoRepositoryBean.class.getName()); + } - /** - * Returns the exclusions to be used when scanning for fragment implementations. - * - * @return - */ - public List getExclusions() { - - Stream configurationExcludes = configuration.getExcludeFilters().stream(); - Stream noRepositoryBeans = Stream.of(new AnnotationTypeFilter(NoRepositoryBean.class)); - - return Stream.concat(configurationExcludes, noRepositoryBeans).collect(StreamUtils.toUnmodifiableList()); - } - - /** - * Returns the name of the implementation class to be detected for the fragment interface. - * - * @return - */ - public String getFragmentImplementationClassName() { - - String postfix = configuration.getRepositoryImplementationPostfix().orElse("Impl"); - - return ClassUtils.getShortName(fragmentInterfaceName).concat(postfix); - } - - /** - * Returns the base packages to be scanned to find implementations of the current fragment interface. - * - * @return - */ - public Iterable getBasePackages() { - return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName)); - } - - private static AnnotationMetadata getAnnotationMetadata(String className, - MetadataReaderFactory metadataReaderFactory) { + private AnnotationMetadata getAnnotationMetadata(String className) { try { - return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata(); + return factory.getMetadataReader(className).getAnnotationMetadata(); + } catch (IOException e) { + throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); + } + } + + private ClassMetadata getClassMetadata(String className) { + + try { + return factory.getMetadataReader(className).getClassMetadata(); } catch (IOException e) { throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); } diff --git a/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java new file mode 100644 index 000000000..16aba7c08 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/ImplementationDetectionConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 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 + * + * http://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; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Expresses configuration to be used to detect implementation classes for repositories and repository fragments. + * + * @author Oliver Gierke + * @since 2.1 + */ +public interface ImplementationDetectionConfiguration { + + /** + * Returns the postfix to be used to calculate the implementation type's name. + * + * @return must not be {@literal null}. + */ + String getImplementationPostfix(); + + /** + * Return the base packages to be scanned for implementation types. + * + * @return must not be {@literal null}. + */ + Streamable getBasePackages(); + + /** + * Returns the exclude filters to be used for the implementation class scanning. + * + * @return must not be {@literal null}. + */ + Streamable getExcludeFilters(); + + /** + * Returns the {@link MetadataReaderFactory} to be used for implementation class scanning. + * + * @return must not be {@literal null}. + */ + MetadataReaderFactory getMetadataReaderFactory(); + + /** + * Generate the bean name for the given {@link BeanDefinition}. + * + * @param definition must not be {@literal null}. + * @return + */ + default String generateBeanName(BeanDefinition definition) { + + Assert.notNull(definition, "BeanDefinition must not be null!"); + + String beanName = definition.getBeanClassName(); + + if (beanName == null) { + throw new IllegalStateException("Cannot generate bean name for BeanDefinition without bean class name!"); + } + + return StringUtils.uncapitalize(ClassUtils.getShortName(beanName)); + } + + /** + * Returns the final lookup configuration for the given fully-qualified fragment interface name. + * + * @param fragmentInterfaceName must not be {@literal null} or empty. + * @return + */ + default ImplementationLookupConfiguration forFragment(String fragmentInterfaceName) { + + Assert.hasText(fragmentInterfaceName, "Fragment interface name must not be null or empty!"); + + return new DefaultImplementationLookupConfiguration(this, fragmentInterfaceName); + } + + /** + * Returns the final lookup configuration for the given {@link RepositoryConfiguration}. + * + * @param config must not be {@literal null}. + * @return + */ + default ImplementationLookupConfiguration forRepositoryConfiguration(RepositoryConfiguration config) { + + Assert.notNull(config, "RepositoryConfiguration must not be null!"); + + return new DefaultImplementationLookupConfiguration(this, config.getRepositoryInterface()) { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.DefaultImplementationLookupConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return config.getImplementationBasePackages(); + } + }; + } +} diff --git a/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java new file mode 100644 index 000000000..82fdb1a3c --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/ImplementationLookupConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 + * + * http://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; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Configuration that's used to lookup an implementation type for a repository or fragment interface. + * + * @author Oliver Gierke + * @since 2.1 + */ +public interface ImplementationLookupConfiguration extends ImplementationDetectionConfiguration { + + /** + * Returns the bean name of the implementation to be looked up. + * + * @return must not be {@literal null}. + */ + String getImplementationBeanName(); + + /** + * Returns the simple name of the class to be looked up. + * + * @return must not be {@literal null}. + */ + String getImplementationClassName(); + + /** + * Return whether the given {@link BeanDefinition} matches the lookup configuration. + * + * @param definition must not be {@literal null}. + * @return + */ + boolean matches(BeanDefinition definition); + + /** + * Returns whether the bean name created for the given bean definition results in the one required. Will be used to + * disambiguate between multiple {@link BeanDefinition}s matching in general. + * + * @param definition must not be {@literal null}. + * @return + * @see #matches(BeanDefinition) + */ + boolean hasMatchingBeanName(BeanDefinition definition); +} 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 b52b67137..6169afb3d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -15,10 +15,6 @@ */ package org.springframework.data.repository.config; -import lombok.RequiredArgsConstructor; - -import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -26,21 +22,17 @@ import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -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.TypeFilter; import org.springframework.data.config.ParsingUtils; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; import org.springframework.data.util.Optionals; -import org.springframework.data.util.Streamable; import org.springframework.util.Assert; /** @@ -60,6 +52,7 @@ class RepositoryBeanDefinitionBuilder { private final ResourceLoader resourceLoader; private final MetadataReaderFactory metadataReaderFactory; + private final FragmentMetadata fragmentMetadata; private final CustomRepositoryImplementationDetector implementationDetector; /** @@ -72,7 +65,7 @@ class RepositoryBeanDefinitionBuilder { * @param environment must not be {@literal null}. */ public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, - ResourceLoader resourceLoader, Environment environment) { + RepositoryConfigurationSource configurationSource, ResourceLoader resourceLoader, Environment environment) { Assert.notNull(extension, "RepositoryConfigurationExtension must not be null!"); Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); @@ -81,9 +74,12 @@ class RepositoryBeanDefinitionBuilder { this.registry = registry; this.extension = extension; this.resourceLoader = resourceLoader; + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); - this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, - resourceLoader); + + this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory); + this.implementationDetector = new CustomRepositoryImplementationDetector(environment, resourceLoader, + configurationSource.toImplementationDetectionConfiguration(metadataReaderFactory)); } /** @@ -135,22 +131,23 @@ class RepositoryBeanDefinitionBuilder { return builder; } - @SuppressWarnings("deprecation") private Optional registerCustomImplementation(RepositoryConfiguration configuration) { - String beanName = configuration.getImplementationBeanName(); + ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory); + + String beanName = lookup.getImplementationBeanName(); // Already a bean configured? if (registry.containsBeanDefinition(beanName)) { return Optional.of(beanName); } - Optional beanDefinition = implementationDetector.detectCustomImplementation(configuration); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); return beanDefinition.map(it -> { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Registering custom repository implementation: " + configuration.getImplementationBeanName() + " " + LOGGER.debug("Registering custom repository implementation: " + lookup.getImplementationBeanName() + " " + it.getBeanClassName()); } @@ -164,27 +161,23 @@ class RepositoryBeanDefinitionBuilder { private Stream registerRepositoryFragmentsImplementation( RepositoryConfiguration configuration) { - ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface()); - RepositoryFragmentDiscovery fragmentConfiguration = new DefaultRepositoryFragmentDiscovery(configuration); + ImplementationDetectionConfiguration config = configuration + .toImplementationDetectionConfiguration(metadataReaderFactory); - return Arrays.stream(classMetadata.getInterfaceNames()) // - .filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) // - .map(it -> FragmentMetadata.of(it, fragmentConfiguration)) // - .map(it -> detectRepositoryFragmentConfiguration(it, configuration.getConfigurationSource())) // + return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) // + .map(it -> detectRepositoryFragmentConfiguration(it, config)) // .flatMap(Optionals::toStream) // .peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) // .peek(it -> potentiallyRegisterRepositoryFragment(configuration, it)); } - private Optional detectRepositoryFragmentConfiguration( - FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) { + private Optional detectRepositoryFragmentConfiguration(String fragmentInterface, + ImplementationDetectionConfiguration config) { - String className = configuration.getFragmentImplementationClassName(); + ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface); + Optional beanDefinition = implementationDetector.detectCustomImplementation(lookup); - Optional beanDefinition = implementationDetector.detectCustomImplementation(className, null, - configuration.getBasePackages(), configuration.getExclusions(), configurationSource::generateBeanName); - - return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd)); + return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd)); } private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration repositoryConfiguration, @@ -232,37 +225,4 @@ class RepositoryBeanDefinitionBuilder { registry.registerBeanDefinition(beanName, ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource())); } - - private ClassMetadata getClassMetadata(String className) { - - try { - return metadataReaderFactory.getMetadataReader(className).getClassMetadata(); - } catch (IOException e) { - throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e); - } - } - - @RequiredArgsConstructor - private static class DefaultRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery { - - private final RepositoryConfiguration configuration; - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters() - */ - @Override - public Streamable getExcludeFilters() { - return configuration.getExcludeFilters(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix() - */ - @Override - public Optional getRepositoryImplementationPostfix() { - return configuration.getConfigurationSource().getRepositoryImplementationPostfix(); - } - } } 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 662e136bb..c97e42f31 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.data.repository.config; import java.util.Optional; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.util.Streamable; @@ -67,24 +68,6 @@ public interface RepositoryConfiguration getNamedQueriesLocation(); - /** - * Returns the class name of the custom implementation. - * - * @return - * @deprecated since 2.0. Use repository compositions by creating mixins. - */ - @Deprecated - String getImplementationClassName(); - - /** - * Returns the bean name of the custom implementation. - * - * @return - * @deprecated since 2.0. Use repository compositions by creating mixins. - */ - @Deprecated - String getImplementationBeanName(); - /** * Returns the name of the repository base class to be used or {@literal null} if the store specific defaults shall be * applied. @@ -129,4 +112,22 @@ public interface RepositoryConfiguration getExcludeFilters(); + + /** + * Returns the {@link ImplementationDetectionConfiguration} to be used for this repository. + * + * @param factory must not be {@literal null}. + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory); + + /** + * Returns the {@link ImplementationLookupConfiguration} for the given {@link MetadataReaderFactory}. + * + * @param factory must not be {@literal null}. + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index 9688ac496..0eec3260b 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -113,8 +113,8 @@ public class RepositoryConfigurationDelegate { extension.registerBeansForRoot(registry, configurationSource); - RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, resourceLoader, - environment); + RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, + configurationSource, resourceLoader, environment); List definitions = new ArrayList<>(); if (LOG.isDebugEnabled()) { diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java index a5c5cea99..9b5ef4206 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.util.Streamable; @@ -126,4 +127,14 @@ public interface RepositoryConfigurationSource { * @since 2.0 */ String generateBeanName(BeanDefinition beanDefinition); + + /** + * Returns the {@link ImplementationDetectionConfiguration} to be used to scan for custom implementations of the + * repository instances to be created from this {@link RepositoryConfigurationSource}. + * + * @param factory + * @return will never be {@literal null}. + * @since 2.1 + */ + ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java index 8154ac649..3da34afec 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSourceSupport.java @@ -15,12 +15,16 @@ */ package org.springframework.data.repository.config; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + import java.util.Collections; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; 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.util.Assert; @@ -117,4 +121,57 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository public boolean shouldConsiderNestedRepositories() { return false; } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationSource#toImplementationDetectionConfiguration() + */ + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + return new SpringImplementationDetectionConfiguration(this, factory); + } + + @RequiredArgsConstructor + private class SpringImplementationDetectionConfiguration implements ImplementationDetectionConfiguration { + + private final RepositoryConfigurationSource source; + private final @Getter MetadataReaderFactory metadataReaderFactory; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix() + */ + @Override + public String getImplementationPostfix() { + return source.getRepositoryImplementationPostfix() + .orElse(DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages() + */ + @Override + public Streamable getBasePackages() { + return source.getBasePackages(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters() + */ + @Override + public Streamable getExcludeFilters() { + return source.getExcludeFilters(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#generateBeanName(org.springframework.beans.factory.config.BeanDefinition) + */ + @Override + public String generateBeanName(BeanDefinition definition) { + return source.generateBeanName(definition); + } + } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java b/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java deleted file mode 100644 index d24988598..000000000 --- a/src/main/java/org/springframework/data/repository/config/RepositoryFragmentDiscovery.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2018 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 - * - * http://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; - -import java.util.Optional; - -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.data.util.Streamable; - -/** - * Interface containing the configurable options for the Spring Data repository fragment subsystem. - * - * @author Mark Paluch - * @since 2.1 - * @see RepositoryConfigurationSource - */ -public interface RepositoryFragmentDiscovery { - - /** - * Returns the {@link TypeFilter}s to be used to exclude packages from repository scanning. - * - * @return - */ - Streamable getExcludeFilters(); - - /** - * Returns the configured postfix to be used for looking up custom implementation classes. - * - * @return the postfix to use or {@link Optional#empty()} in case none is configured. - */ - Optional getRepositoryImplementationPostfix(); -} diff --git a/src/main/java/org/springframework/data/util/Lazy.java b/src/main/java/org/springframework/data/util/Lazy.java index 840c2d285..4b2d36be2 100644 --- a/src/main/java/org/springframework/data/util/Lazy.java +++ b/src/main/java/org/springframework/data/util/Lazy.java @@ -15,6 +15,8 @@ */ package org.springframework.data.util; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -35,9 +37,12 @@ import org.springframework.util.Assert; * @since 2.0 */ @RequiredArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode public class Lazy implements Supplier { + private static final Lazy EMPTY = new Lazy<>(() -> null, null, true); + private final Supplier supplier; private @Nullable T value = null; private boolean resolved = false; @@ -67,6 +72,17 @@ public class Lazy implements Supplier { return new Lazy<>(() -> value); } + /** + * Creates a pre-resolved empty {@link Lazy}. + * + * @return + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public static Lazy empty() { + return (Lazy) EMPTY; + } + /** * Returns the value created by the configured {@link Supplier}. Will return the calculated instance for subsequent * lookups. diff --git a/src/main/java/org/springframework/data/util/Streamable.java b/src/main/java/org/springframework/data/util/Streamable.java index a3a0802e4..d489083b6 100644 --- a/src/main/java/org/springframework/data/util/Streamable.java +++ b/src/main/java/org/springframework/data/util/Streamable.java @@ -30,9 +30,10 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Christoph Strobl + * @since 2.0 */ @FunctionalInterface -public interface Streamable extends Iterable { +public interface Streamable extends Iterable, Supplier> { /** * Returns an empty {@link Streamable}. @@ -130,4 +131,26 @@ public interface Streamable extends Iterable { default boolean isEmpty() { return !iterator().hasNext(); } + + /** + * Creates a new {@link Streamable} from the current one and the given {@link Stream} concatenated. + * + * @param stream must not be {@literal null}. + * @return + * @since 2.1 + */ + default Streamable and(Supplier> stream) { + + Assert.notNull(stream, "Stream must not be null!"); + + return Streamable.of(() -> Stream.concat(this.stream(), stream.get())); + } + + /* + * (non-Javadoc) + * @see java.util.function.Supplier#get() + */ + default Stream get() { + return stream(); + } } diff --git a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java index 69c584cb4..e42a346fb 100755 --- a/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/repository/cdi/CdiRepositoryBeanUnitTests.java @@ -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 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 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 89641d89b..675673cd1 100644 --- a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java @@ -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 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 beanDefinition = detector.detectCustomImplementation("className", "beanName", emptyList(), - emptyList(), nameGenerator); + ImplementationLookupConfiguration lookup = configuration + .forRepositoryConfiguration(configFor(NoImplementationRepository.class)); + + Optional 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 beanDefinition = detector.detectCustomImplementation(lookup); - Optional 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 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 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 {} } } diff --git a/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java b/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java index e67f15720..0de15c311 100755 --- a/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/DefaultRepositoryConfigurationUnitTests.java @@ -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 answer = invocation -> generator.generateBeanName((BeanDefinition) invocation.getArguments()[0]); - when(source.generateBeanName(Mockito.any(BeanDefinition.class))).then(answer); - } - @Test public void supportsBasicConfiguration() { RepositoryConfiguration 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();