DATACMNS-1233 - Allow CDI Repositories to be composed of an arbitrary number of implementation classes.

We now support repository fragments for repositories exported through CDI.

Original pull request: #272.
This commit is contained in:
Mark Paluch
2018-02-07 14:36:21 +01:00
committed by Oliver Gierke
parent 919090bf20
commit 0a3b71f0a1
22 changed files with 1006 additions and 214 deletions

View File

@@ -25,12 +25,12 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.Alternative;
import javax.enterprise.inject.Stereotype;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionPoint;
@@ -38,15 +38,13 @@ import javax.enterprise.inject.spi.PassivationCapable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.RepositoryBeanNameGenerator;
import org.springframework.data.repository.config.SpringDataAnnotationBeanNameGenerator;
import org.springframework.data.repository.config.RepositoryFragmentConfiguration;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -66,16 +64,12 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
private final Set<Annotation> qualifiers;
private final Class<T> repositoryType;
private final Optional<CustomRepositoryImplementationDetector> detector;
private final CdiRepositoryContext context;
private final BeanManager beanManager;
private final String passivationId;
private transient @Nullable T repoInstance;
private final SpringDataAnnotationBeanNameGenerator annotationBeanNameGenerator = new SpringDataAnnotationBeanNameGenerator();
private final RepositoryBeanNameGenerator beanNameGenerator = new RepositoryBeanNameGenerator(
getClass().getClassLoader());
/**
* Creates a new {@link CdiRepositoryBean}.
*
@@ -84,7 +78,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
* @param beanManager the CDI {@link BeanManager}, must not be {@literal null}.
*/
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager) {
this(qualifiers, repositoryType, beanManager, Optional.empty());
this(qualifiers, repositoryType, beanManager, new CdiRepositoryContext(CdiRepositoryBean.class.getClassLoader()));
}
/**
@@ -93,8 +87,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
* @param qualifiers must not be {@literal null}.
* @param repositoryType has to be an interface must not be {@literal null}.
* @param beanManager the CDI {@link BeanManager}, must not be {@literal null}.
* @param detector detector for the custom repository implementations {@link CustomRepositoryImplementationDetector},
* can be {@literal null}.
* @param detector detector for the custom repository implementations {@link CustomRepositoryImplementationDetector}.
*/
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager,
Optional<CustomRepositoryImplementationDetector> detector) {
@@ -107,7 +100,32 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
this.qualifiers = qualifiers;
this.repositoryType = repositoryType;
this.beanManager = beanManager;
this.detector = detector;
this.context = new CdiRepositoryContext(getClass().getClassLoader(), detector
.orElseThrow(() -> new IllegalArgumentException("CustomRepositoryImplementationDetector must be present!")));
this.passivationId = createPassivationId(qualifiers, repositoryType);
}
/**
* Creates a new {@link CdiRepositoryBean}.
*
* @param qualifiers must not be {@literal null}.
* @param repositoryType has to be an interface must not be {@literal null}.
* @param beanManager the CDI {@link BeanManager}, must not be {@literal null}.
* @param context CDI context encapsulating class loader, metadata scanning and fragment detection.
* @since 2.1
*/
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager,
CdiRepositoryContext context) {
Assert.notNull(qualifiers, "Qualifiers must not be null!");
Assert.notNull(beanManager, "BeanManager must not be null!");
Assert.notNull(repositoryType, "Repoitory type must not be null!");
Assert.isTrue(repositoryType.isInterface(), "RepositoryType must be an interface!");
this.qualifiers = qualifiers;
this.repositoryType = repositoryType;
this.beanManager = beanManager;
this.context = context;
this.passivationId = createPassivationId(qualifiers, repositoryType);
}
@@ -128,7 +146,6 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
Collections.sort(qualifierNames);
return StringUtils.collectionToDelimitedString(qualifierNames, ":") + ":" + repositoryType.getName();
}
/*
@@ -215,89 +232,6 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
creationalContext.release();
}
/**
* Looks up an instance of a {@link CdiRepositoryConfiguration}. In case the instance cannot be found within the CDI
* scope, a default configuration is used.
*
* @return an available CdiRepositoryConfiguration instance or a default configuration.
*/
protected CdiRepositoryConfiguration lookupConfiguration(BeanManager beanManager, Set<Annotation> qualifiers) {
return beanManager.getBeans(CdiRepositoryConfiguration.class, getQualifiersArray(qualifiers)).stream().findFirst()//
.map(it -> (CdiRepositoryConfiguration) getDependencyInstance(it)) //
.orElse(DEFAULT_CONFIGURATION);
}
/**
* Try to lookup a custom implementation for a {@link org.springframework.data.repository.Repository}. Can only be
* used when a {@code CustomRepositoryImplementationDetector} is provided.
*
* @param repositoryType
* @param beanManager
* @param qualifiers
* @return the custom implementation instance or null
*/
private Optional<Bean<?>> getCustomImplementationBean(Class<?> repositoryType, BeanManager beanManager,
Set<Annotation> qualifiers) {
return detector.flatMap(it -> {
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
return getCustomImplementationClass(repositoryType, cdiRepositoryConfiguration, it)//
.flatMap(type -> beanManager.getBeans(type, getQualifiersArray(qualifiers)).stream().findFirst());
});
}
/**
* Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can
* find also a custom repository which is inherited over many levels.
*
* @param repositoryType The class representing the repository.
* @param cdiRepositoryConfiguration The configuration for CDI usage.
* @return the interface class or {@literal null}.
*/
private Optional<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration, CustomRepositoryImplementationDetector detector) {
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
className, //
getCustomImplementationBeanName(repositoryType), //
Collections.singleton(repositoryType.getPackage().getName()), //
Collections.emptySet(), //
beanNameGenerator::generateBeanName //
);
return beanDefinition.map(it -> {
try {
return Class.forName(it.getBeanClassName());
} catch (ClassNotFoundException e) {
throw new UnsatisfiedResolutionException(
String.format("Unable to resolve class for '%s'", it.getBeanClassName()), e);
}
});
}
private String getCustomImplementationBeanName(Class<?> repositoryType) {
return annotationBeanNameGenerator.generateBeanName(new AnnotatedGenericBeanDefinition(repositoryType))
+ DEFAULT_CONFIGURATION.getRepositoryImplementationPostfix();
}
private 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;
}
private Annotation[] getQualifiersArray(Set<Annotation> qualifiers) {
return qualifiers.toArray(new Annotation[qualifiers.size()]);
}
/*
* (non-Javadoc)
* @see javax.enterprise.inject.spi.Bean#getQualifiers()
@@ -380,18 +314,75 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
* @param creationalContext will never be {@literal null}.
* @param repositoryType will never be {@literal null}.
* @return
* @deprecated override {@link #create(CreationalContext, Class, Object)} instead.
*/
@Deprecated
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType, beanManager, qualifiers);
Optional<Object> customImplementation = customImplementationBean
.map(it -> beanManager.getReference(it, it.getBeanClass(), beanManager.createCreationalContext(it)));
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
cdiRepositoryConfiguration);
Optional<Object> customImplementation = customImplementationBean.map(this::getDependencyInstance);
return create(creationalContext, repositoryType, customImplementation);
}
/**
* Lookup repository fragments for a {@link Class repository interface}.
*
* @param repositoryType must not be {@literal null}.
* @return the {@link RepositoryFragments}.
* @since 2.1
*/
protected RepositoryFragments getRepositoryFragments(Class<T> repositoryType) {
Assert.notNull(repositoryType, "Repository type must not be null!");
CdiRepositoryConfiguration cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType,
cdiRepositoryConfiguration);
Optional<Object> customImplementation = customImplementationBean.map(this::getDependencyInstance);
List<RepositoryFragment<?>> repositoryFragments = getRepositoryFragments(repositoryType,
cdiRepositoryConfiguration);
RepositoryFragments customImplementationFragment = customImplementation //
.map(RepositoryFragments::just) //
.orElseGet(RepositoryFragments::empty);
return RepositoryFragments.from(repositoryFragments) //
.append(customImplementationFragment);
}
@SuppressWarnings("unchecked")
private List<RepositoryFragment<?>> getRepositoryFragments(Class<T> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
Stream<RepositoryFragmentConfiguration> fragmentConfigurations = context
.getRepositoryFragments(cdiRepositoryConfiguration, repositoryType);
return fragmentConfigurations.flatMap(it -> {
Class<Object> interfaceClass = (Class) lookupFragmentInterface(repositoryType, it.getInterfaceName());
Class<?> implementationClass = context.loadClass(it.getClassName());
Optional<Bean<?>> bean = getBean(implementationClass, beanManager, qualifiers);
return bean.map(this::getDependencyInstance) //
.map(implementation -> RepositoryFragment.implemented(interfaceClass, implementation)) //
.map(Stream::of) //
.orElse(Stream.empty());
}).collect(Collectors.toList());
}
private static Class<?> lookupFragmentInterface(Class<?> repositoryType, String interfaceName) {
return Arrays.stream(repositoryType.getInterfaces()) //
.filter(it -> it.getName().equals(interfaceName)) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(String.format("Did not find type %s in %s!", interfaceName,
Arrays.asList(repositoryType.getInterfaces()))));
}
/**
* Creates the actual component instance.
*
@@ -399,7 +390,10 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
* @param repositoryType will never be {@literal null}.
* @param customImplementation can be {@literal null}.
* @return
* @deprecated since 2.1, override {@link #create(CreationalContext, Class)} in which you create a repository factory
* and call {@link #create(RepositoryFactorySupport, Class, RepositoryFragments)}.
*/
@Deprecated
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType,
Optional<Object> customImplementation) {
throw new UnsupportedOperationException(
@@ -407,6 +401,35 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
+ "in order to use custom repository implementations");
}
/**
* Looks up an instance of a {@link CdiRepositoryConfiguration}. In case the instance cannot be found within the CDI
* scope, a default configuration is used.
*
* @return an available CdiRepositoryConfiguration instance or a default configuration.
*/
protected CdiRepositoryConfiguration lookupConfiguration(BeanManager beanManager, Set<Annotation> qualifiers) {
return beanManager.getBeans(CdiRepositoryConfiguration.class, getQualifiersArray(qualifiers)).stream().findFirst()//
.map(it -> (CdiRepositoryConfiguration) getDependencyInstance(it)) //
.orElse(DEFAULT_CONFIGURATION);
}
/**
* Try to lookup a custom implementation for a {@link org.springframework.data.repository.Repository}. Can only be
* used when a {@code CustomRepositoryImplementationDetector} is provided.
*
* @param repositoryType
* @param beanManager
* @param qualifiers
* @return the custom implementation instance or null
*/
private Optional<Bean<?>> getCustomImplementationBean(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
return context.getCustomImplementationClass(repositoryType, cdiRepositoryConfiguration)//
.flatMap(type -> getBean(type, beanManager, qualifiers));
}
/**
* Applies the configuration from {@link CdiRepositoryConfiguration} to {@link RepositoryFactorySupport} by looking up
* the actual configuration.
@@ -435,6 +458,26 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
configuration.getRepositoryBeanClass().ifPresent(repositoryFactory::setRepositoryBaseClass);
}
/**
* Creates the actual repository instance.
*
* @param repositoryType will never be {@literal null}.
* @param repositoryFragments will never be {@literal null}.
* @return
*/
protected static <T> T create(RepositoryFactorySupport repositoryFactory, Class<T> repositoryType,
RepositoryFragments repositoryFragments) {
return repositoryFactory.getRepository(repositoryType, repositoryFragments);
}
private static Optional<Bean<?>> getBean(Class<?> beanType, BeanManager beanManager, Set<Annotation> qualifiers) {
return beanManager.getBeans(beanType, getQualifiersArray(qualifiers)).stream().findFirst();
}
private static Annotation[] getQualifiersArray(Set<Annotation> qualifiers) {
return qualifiers.toArray(new Annotation[qualifiers.size()]);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()

View File

@@ -0,0 +1,217 @@
/*
* 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.cdi;
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.RepositoryFragmentConfiguration;
import org.springframework.data.repository.config.RepositoryFragmentDiscovery;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Context for CDI repositories. This class provides {@link ClassLoader} and
* {@link org.springframework.data.repository.core.support.RepositoryFragment detection} which are commonly used within
* CDI.
*
* @author Mark Paluch
* @since 2.1
*/
public class CdiRepositoryContext {
private final ClassLoader classLoader;
private final CustomRepositoryImplementationDetector detector;
private final MetadataReaderFactory metadataReaderFactory;
/**
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize
* {@link CachingMetadataReaderFactory}.
*
* @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);
}
/**
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and
* {@link CustomRepositoryImplementationDetector}.
*
* @param classLoader must not be {@literal null}.
* @param detector must not be {@literal null}.
*/
public CdiRepositoryContext(ClassLoader classLoader, CustomRepositoryImplementationDetector detector) {
Assert.notNull(classLoader, "ClassLoader must not be null!");
Assert.notNull(detector, "CustomRepositoryImplementationDetector must not be null!");
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader);
this.classLoader = classLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.detector = detector;
}
CustomRepositoryImplementationDetector getCustomRepositoryImplementationDetector() {
return detector;
}
/**
* Load a {@link Class} using the CDI {@link ClassLoader}.
*
* @param className
* @return
* @throws UnsatisfiedResolutionException if the class cannot be found.
*/
Class<?> loadClass(String className) {
try {
return ClassUtils.forName(className, classLoader);
} catch (ClassNotFoundException e) {
throw new UnsatisfiedResolutionException(String.format("Unable to resolve class for '%s'", className), e);
}
}
/**
* Discover {@link RepositoryFragmentConfiguration fragment configurations} for a {@link Class repository interface}.
*
* @param configuration must not be {@literal null}.
* @param repositoryInterface must not be {@literal null}.
* @return {@link Stream} of {@link RepositoryFragmentConfiguration fragment configurations}.
*/
Stream<RepositoryFragmentConfiguration> getRepositoryFragments(CdiRepositoryConfiguration configuration,
Class<?> repositoryInterface) {
ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName());
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) //
.flatMap(Optionals::toStream);
}
/**
* Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can
* find also a custom repository which is inherited over many levels.
*
* @param repositoryType The class representing the repository.
* @param cdiRepositoryConfiguration The configuration for CDI usage.
* @return the interface class or {@literal null}.
*/
Optional<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
className, //
className, Collections.singleton(repositoryType.getPackage().getName()), //
Collections.emptySet(), //
BeanDefinition::getBeanClassName);
return beanDefinition.map(it -> loadClass(it.getBeanClassName()));
}
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
FragmentMetadata configuration) {
String className = configuration.getFragmentImplementationClassName();
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(className, null,
configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName);
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
}
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 final CdiRepositoryConfiguration configuration;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
*/
@Override
public Optional<String> getRepositoryImplementationPostfix() {
return Optional.of(configuration.getRepositoryImplementationPostfix());
}
}
}

View File

@@ -36,12 +36,6 @@ import javax.inject.Qualifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
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.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.RepositoryDefinition;
@@ -61,16 +55,10 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
private final Map<Class<?>, Set<Annotation>> repositoryTypes = new HashMap<>();
private final Set<CdiRepositoryBean<?>> eagerRepositories = new HashSet<>();
private final CustomRepositoryImplementationDetector customImplementationDetector;
private final CdiRepositoryContext context;
protected CdiRepositoryExtensionSupport() {
Environment environment = new StandardEnvironment();
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.customImplementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment,
resourceLoader);
context = new CdiRepositoryContext(getClass().getClassLoader());
}
/**
@@ -91,8 +79,8 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
Set<Annotation> qualifiers = getQualifiers(repositoryType);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(),
qualifiers));
LOGGER.debug(
String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(), qualifiers));
}
// Store the repository type using its qualifiers.
repositoryTypes.put(repositoryType, qualifiers);
@@ -184,7 +172,15 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
* @return the {@link CustomRepositoryImplementationDetector} to scan for the custom implementation
*/
protected CustomRepositoryImplementationDetector getCustomImplementationDetector() {
return customImplementationDetector;
return context.getCustomRepositoryImplementationDetector();
}
/**
* @return the {@link CdiRepositoryContext} encapsulating the CDI-specific class loaders and fragment scanning.
* @since 2.1
*/
protected CdiRepositoryContext getRepositoryContext() {
return context;
}
@SuppressWarnings("all")

View File

@@ -0,0 +1,107 @@
/*
* 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 lombok.Value;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.core.type.AnnotationMetadata;
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
* @since 2.1
*/
@Value(staticConstructor = "of")
public class FragmentMetadata {
private String fragmentInterfaceName;
private RepositoryFragmentDiscovery configuration;
/**
* Returns whether the given interface is a fragment candidate.
*
* @param interfaceName must not be {@literal null} or empty.
* @param factory must not be {@literal null}.
* @return
*/
public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) {
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory);
return !metadata.hasAnnotation(NoRepositoryBean.class.getName());
}
/**
* Returns the exclusions to be used when scanning for fragment implementations.
*
* @return
*/
public List<TypeFilter> getExclusions() {
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
Stream<AnnotationTypeFilter> 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<String> getBasePackages() {
return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName));
}
private static AnnotationMetadata getAnnotationMetadata(String className,
MetadataReaderFactory metadataReaderFactory) {
try {
return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata();
} catch (IOException e) {
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
}
}
}

View File

@@ -15,42 +15,35 @@
*/
package org.springframework.data.repository.config;
import lombok.Value;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
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.config.ParsingUtils;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean;
import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances.
@@ -180,23 +173,24 @@ class RepositoryBeanDefinitionBuilder {
RepositoryConfiguration<?> configuration) {
ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface());
RepositoryFragmentDiscovery fragmentConfiguration = new DefaultRepositoryFragmentDiscovery(configuration);
return Arrays.stream(classMetadata.getInterfaceNames()) //
.filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) //
.map(it -> FragmentMetadata.of(it, configuration)) //
.map(it -> detectRepositoryFragmentConfiguration(it)) //
.flatMap(it -> Optionals.toStream(it)) //
.map(it -> FragmentMetadata.of(it, fragmentConfiguration)) //
.map(it -> detectRepositoryFragmentConfiguration(it, configuration.getConfigurationSource())) //
.flatMap(Optionals::toStream) //
.peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) //
.peek(it -> potentiallyRegisterRepositoryFragment(configuration, it));
}
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
FragmentMetadata configuration) {
FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) {
String className = configuration.getFragmentImplementationClassName();
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(className, null,
configuration.getBasePackages(), configuration.getExclusions(), configuration.getBeanNameGenerator());
configuration.getBasePackages(), configuration.getExclusions(), configurationSource::generateBeanName);
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
}
@@ -256,81 +250,27 @@ class RepositoryBeanDefinitionBuilder {
}
}
@Value(staticConstructor = "of")
static class FragmentMetadata {
@RequiredArgsConstructor
private static class DefaultRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery {
String fragmentInterfaceName;
RepositoryConfiguration<?> configuration;
private final RepositoryConfiguration<?> configuration;
/**
* Returns whether the given interface is a fragment candidate.
*
* @param interfaceName must not be {@literal null} or empty.
* @param factory must not be {@literal null}.
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
*/
public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) {
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory);
return !metadata.hasAnnotation(NoRepositoryBean.class.getName());
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return configuration.getExcludeFilters();
}
/**
* Returns the exclusions to be used when scanning for fragment implementations.
*
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
*/
public List<TypeFilter> getExclusions() {
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
Stream<AnnotationTypeFilter> 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() {
RepositoryConfigurationSource configurationSource = configuration.getConfigurationSource();
String postfix = configurationSource.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<String> getBasePackages() {
return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName));
}
/**
* Returns the bean name generating function to be used for the fragment.
*
* @return
*/
public Function<BeanDefinition, String> getBeanNameGenerator() {
return definition -> configuration.getConfigurationSource().generateBeanName(definition);
}
private static AnnotationMetadata getAnnotationMetadata(String className,
MetadataReaderFactory metadataReaderFactory) {
try {
return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata();
} catch (IOException e) {
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
}
@Override
public Optional<String> getRepositoryImplementationPostfix() {
return configuration.getConfigurationSource().getRepositoryImplementationPostfix();
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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<TypeFilter> 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<String> getRepositoryImplementationPostfix();
}