DATACMNS-764 - Disambiguate custom repository implementation if necessary

When multiple repository implementations are found based on the class name, the one with a bean name matching the interfaces bean name + implementation postfix is picked. Includes support for CDI.

RepositoryBeanNameGenerator now no longer implements BeanNameGenerator since while it produces names it does not behave like the interface suggests, i.e. it can work without a BeanFactory in the first place. It now uses constructor injection and is package private.

Original pull request: #201.
This commit is contained in:
Jens Schauder
2017-03-07 17:06:58 +01:00
committed by Oliver Gierke
parent 628d71cf1f
commit eec63cb11d
17 changed files with 526 additions and 75 deletions

View File

@@ -38,9 +38,12 @@ 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.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.DefaultRepositoryConfiguration;
import org.springframework.data.repository.config.RepositoryBeanNameGenerator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -52,6 +55,7 @@ import org.springframework.util.StringUtils;
* @author Oliver Gierke
* @author Mark Paluch
* @author Peter Rietzler
* @author Jens Schauder
* @author Christoph Strobl
*/
public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapable {
@@ -67,6 +71,10 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
private transient T repoInstance;
private final AnnotationBeanNameGenerator annotationBeanNameGenerator = new AnnotationBeanNameGenerator();
private final RepositoryBeanNameGenerator beanNameGenerator =
new RepositoryBeanNameGenerator(getClass().getClassLoader());
/**
* Creates a new {@link CdiRepositoryBean}.
*
@@ -247,8 +255,13 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
CdiRepositoryConfiguration cdiRepositoryConfiguration, CustomRepositoryImplementationDetector detector) {
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(className,
Collections.singleton(repositoryType.getPackage().getName()), Collections.emptySet());
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
className, //
getCustomImplementationBeanName(repositoryType), //
Collections.singleton(repositoryType.getPackage().getName()), //
Collections.emptySet(), //
beanNameGenerator::generateBeanName //
);
return beanDefinition.map(it -> {
@@ -261,6 +274,11 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
});
}
private String getCustomImplementationBeanName(Class<?> repositoryType) {
return annotationBeanNameGenerator.generateBeanName(new AnnotatedGenericBeanDefinition(repositoryType), null)
+ DEFAULT_CONFIGURATION.getRepositoryImplementationPostfix();
}
private String getCustomImplementationClassName(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
@@ -356,8 +374,10 @@ 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 overide {@link #create(CreationalContext, Class, Object)} instead.
*/
private T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
@Deprecated
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
Optional<Bean<?>> customImplementationBean = getCustomImplementationBean(repositoryType, beanManager, qualifiers);
Optional<Object> customImplementation = customImplementationBean

View File

@@ -47,6 +47,7 @@ import org.springframework.util.StringUtils;
* @author Oliver Gierke
* @author Thomas Darimont
* @author Peter Rietzler
* @author Jens Schauder
*/
public class AnnotationRepositoryConfigurationSource extends RepositoryConfigurationSourceSupport {
@@ -77,7 +78,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
public AnnotationRepositoryConfigurationSource(AnnotationMetadata metadata, Class<? extends Annotation> annotation,
ResourceLoader resourceLoader, Environment environment) {
super(environment);
super(environment, resourceLoader.getClassLoader());
Assert.notNull(metadata, "Metadata must not be null!");
Assert.notNull(annotation, "Annotation must not be null!");

View File

@@ -19,9 +19,13 @@ package org.springframework.data.repository.config;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -37,11 +41,12 @@ import org.springframework.util.Assert;
/**
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository}
*
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
* @author Peter Rietzler
* @author Jens Schauder
*/
@RequiredArgsConstructor
public class CustomRepositoryImplementationDetector {
@@ -54,7 +59,7 @@ public class CustomRepositoryImplementationDetector {
/**
* 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.
*/
@@ -62,24 +67,44 @@ public class CustomRepositoryImplementationDetector {
// TODO 2.0: Extract into dedicated interface for custom implementation lookup configuration.
return detectCustomImplementation(configuration.getImplementationClassName(), //
return detectCustomImplementation( //
configuration.getImplementationClassName(), //
configuration.getImplementationBeanName(), //
configuration.getBasePackages(), //
configuration.getExcludeFilters());
configuration.getExcludeFilters(), //
bd -> configuration.getConfigurationSource().generateBeanName(bd));
}
/**
* 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}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
*/
public Optional<AbstractBeanDefinition> detectCustomImplementation(String className, Iterable<String> basePackages,
Iterable<TypeFilter> excludeFilters) {
public Optional<AbstractBeanDefinition> detectCustomImplementation(String className, String beanName,
Iterable<String> basePackages, Iterable<TypeFilter> excludeFilters,
Function<BeanDefinition, String> beanNameGenerator) {
Assert.notNull(className, "ClassName must not be null!");
Assert.notNull(basePackages, "BasePackages must not be null!");
Set<BeanDefinition> definitions = findCandidateBeanDefinitions(className, basePackages, excludeFilters);
SelectionSet<BeanDefinition> selection = new SelectionSet<>( //
definitions, //
c -> c.isEmpty() ? null : throwAmbiguousCustomImplementationException(c) //
).filterIfNecessary(bd -> beanName != null && beanName.equals(beanNameGenerator.apply(bd)));
return Optional.ofNullable((AbstractBeanDefinition) selection.uniqueResult());
}
Set<BeanDefinition> findCandidateBeanDefinitions(String className, Iterable<String> basePackages,
Iterable<TypeFilter> excludeFilters) {
// Build pattern to lookup implementation class
Pattern pattern = Pattern.compile(".*\\." + className);
@@ -101,12 +126,15 @@ public class CustomRepositoryImplementationDetector {
definitions.addAll(provider.findCandidateComponents(basePackage));
}
if (definitions.isEmpty()) {
return Optional.empty();
}
return definitions;
}
if (definitions.size() == 1) {
return Optional.of((AbstractBeanDefinition) definitions.iterator().next());
private static AbstractBeanDefinition throwAmbiguousCustomImplementationException(
Collection<BeanDefinition> definitions) {
List<String> implementationClassNames = new ArrayList<String>();
for (BeanDefinition bean : definitions) {
implementationClassNames.add(bean.getBeanClassName());
}
throw new IllegalStateException(

View File

@@ -28,8 +28,9 @@ import org.springframework.util.StringUtils;
/**
* Default implementation of {@link RepositoryConfiguration}.
*
*
* @author Oliver Gierke
* @author Jens Schauder
*/
@RequiredArgsConstructor
public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSource>
@@ -104,7 +105,8 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou
* @see org.springframework.data.repository.config.RepositoryConfiguration#getImplementationBeanName()
*/
public String getImplementationBeanName() {
return StringUtils.uncapitalize(getImplementationClassName());
return configurationSource.generateBeanName(definition)
+ configurationSource.getRepositoryImplementationPostfix().orElse("Impl");
}
/*

View File

@@ -124,8 +124,7 @@ class RepositoryBeanDefinitionBuilder {
return Optional.of(beanName);
}
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(
configuration.getImplementationClassName(), configuration.getBasePackages(), configuration.getExcludeFilters());
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(configuration);
return beanDefinition.map(it -> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2017 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.
@@ -15,56 +15,69 @@
*/
package org.springframework.data.repository.config;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.ScannedGenericBeanDefinition;
import org.springframework.util.ClassUtils;
/**
* Special {@link BeanNameGenerator} to create bean names for Spring Data repositories. Will delegate to an
* {@link AnnotationBeanNameGenerator} but let the delegate work with a customized {@link BeanDefinition} to make sure
* the repository interface is inspected and not the actual bean definition class.
*
*
* @author Oliver Gierke
* @author Jens Schauder
*/
public class RepositoryBeanNameGenerator implements BeanNameGenerator, BeanClassLoaderAware {
public class RepositoryBeanNameGenerator {
private static final BeanNameGenerator DELEGATE = new AnnotationBeanNameGenerator();
private ClassLoader beanClassLoader;
private final ClassLoader beanClassLoader;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
public RepositoryBeanNameGenerator(ClassLoader beanClassLoader) {
this.beanClassLoader = beanClassLoader;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.support.BeanNameGenerator#generateBeanName(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry)
/**
* Generate a bean name for the given bean definition.
*
* @param definition the bean definition to generate a name for
* @return the generated bean name
* @since 2.0
*/
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
public String generateBeanName(BeanDefinition definition) {
AnnotatedBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(getRepositoryInterfaceFrom(definition));
return DELEGATE.generateBeanName(beanDefinition, registry);
return DELEGATE.generateBeanName(beanDefinition, null);
}
/**
* Returns the type configured for the {@code repositoryInterface} property of the given bean definition. Uses a
* potential {@link Class} being configured as is or tries to load a class with the given value's {@link #toString()}
* representation.
*
*
* @param beanDefinition
* @return
*/
private Class<?> getRepositoryInterfaceFrom(BeanDefinition beanDefinition) {
if (beanDefinition instanceof ScannedGenericBeanDefinition) {
try {
return ((ScannedGenericBeanDefinition) beanDefinition).resolveBeanClass(beanClassLoader);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Could not resolve bean class.", e);
}
} else {
return getRepositoryInterfaceFromFactory(beanDefinition);
}
}
private Class<?> getRepositoryInterfaceFromFactory(BeanDefinition beanDefinition) {
Object value = beanDefinition.getConstructorArgumentValues().getArgumentValue(0, Class.class).getValue();
if (value instanceof Class<?>) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -24,7 +24,6 @@ import org.springframework.beans.factory.parsing.BeanComponentDefinition;
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.BeanNameGenerator;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
@@ -38,8 +37,9 @@ import org.springframework.util.Assert;
* providing a configuration format specific {@link RepositoryConfigurationSource} (currently either XML or annotations
* are supported). The actual registration can then be triggered for different {@link RepositoryConfigurationExtension}
* s.
*
*
* @author Oliver Gierke
* @author Jens Schauder
*/
public class RepositoryConfigurationDelegate {
@@ -53,7 +53,6 @@ public class RepositoryConfigurationDelegate {
private final RepositoryConfigurationSource configurationSource;
private final ResourceLoader resourceLoader;
private final Environment environment;
private final BeanNameGenerator beanNameGenerator;
private final boolean isXml;
private final boolean inMultiStoreMode;
@@ -75,10 +74,6 @@ public class RepositoryConfigurationDelegate {
"Configuration source must either be an Xml- or an AnnotationBasedConfigurationSource!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
RepositoryBeanNameGenerator generator = new RepositoryBeanNameGenerator();
generator.setBeanClassLoader(resourceLoader.getClassLoader());
this.beanNameGenerator = generator;
this.configurationSource = configurationSource;
this.resourceLoader = resourceLoader;
this.environment = defaultEnvironment(environment, resourceLoader);
@@ -133,7 +128,7 @@ public class RepositoryConfigurationDelegate {
}
AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition();
String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
String beanName = configurationSource.generateBeanName(beanDefinition);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(REPOSITORY_REGISTRATION, extension.getModuleName(), beanName,

View File

@@ -29,6 +29,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
* @author Oliver Gierke
* @author Thomas Darimont
* @author Peter Rietzler
* @author Jens Schauder
*/
public interface RepositoryConfigurationSource {
@@ -115,4 +116,13 @@ public interface RepositoryConfigurationSource {
* @return must not be {@literal null}.
*/
Iterable<TypeFilter> getExcludeFilters();
/**
* Returns a name for the beanDefinition.
*
* @param beanDefinition Must not be {@literal null}.
* @return
* @since 2.0
*/
String generateBeanName(BeanDefinition beanDefinition);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2017 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.
@@ -28,26 +28,32 @@ import org.springframework.util.Assert;
/**
* Base class to implement {@link RepositoryConfigurationSource}s.
*
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Peter Rietzler
* @author Jens Schauder
*/
public abstract class RepositoryConfigurationSourceSupport implements RepositoryConfigurationSource {
protected static final String DEFAULT_REPOSITORY_IMPL_POSTFIX = "Impl";
private final Environment environment;
private final RepositoryBeanNameGenerator beanNameGenerator;
/**
* Creates a new {@link RepositoryConfigurationSourceSupport} with the given environment.
*
*
* @param environment must not be {@literal null}.
* @param classLoader must not be {@literal null}.
*/
public RepositoryConfigurationSourceSupport(Environment environment) {
public RepositoryConfigurationSourceSupport(Environment environment, ClassLoader classLoader) {
Assert.notNull(environment, "Environment must not be null!");
Assert.notNull(classLoader, "ClassLoader must not be null!");
this.environment = environment;
this.beanNameGenerator = new RepositoryBeanNameGenerator(classLoader);
}
/*
@@ -78,7 +84,7 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository
/**
* Return the {@link TypeFilter}s to define which types to exclude when scanning for repositories. Default
* implementation returns an empty collection.
*
*
* @return must not be {@literal null}.
*/
public Iterable<TypeFilter> getExcludeFilters() {
@@ -88,7 +94,7 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository
/**
* Return the {@link TypeFilter}s to define which types to include when scanning for repositories. Default
* implementation returns an empty collection.
*
*
* @return must not be {@literal null}.
*/
protected Iterable<TypeFilter> getIncludeFilters() {
@@ -98,10 +104,19 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository
/**
* Returns whether we should consider nested repositories, i.e. repository interface definitions nested in other
* classes.
*
*
* @return {@literal true} if the container should look for nested repository interface definitions.
*/
public boolean shouldConsiderNestedRepositories() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#getBeanNameGenerator()
*/
@Override
public String generateBeanName(BeanDefinition beanDefinition) {
return beanNameGenerator.generateBeanName(beanDefinition);
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2017 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.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Allows filtering of a collection in order to select a unique element. Once a unique element is found all further
* filters are ignored.
*
* @author Jens Schauder
*/
class SelectionSet<T> {
private final Collection<T> collection;
private final Function<Collection<T>, T> fallback;
/**
* creates a {@link SelectionSet} with a default fallback of {@literal null}, when no element is found and an
* {@link IllegalStateException} when no element is found.
*/
SelectionSet(Collection<T> collection) {
this(collection, defaultFallback());
}
/**
* @param collection The collection from which a unique element will get picked.
* @param fallback Will get called once {@literal #uniqueResult} gets called if no unique result can be determined.
*/
SelectionSet(Collection<T> collection, Function<Collection<T>, T> fallback) {
this.collection = collection;
this.fallback = fallback;
}
/**
* If this <code>SelectionSet</code> contains exactly one element it gets returned. If no unique result can
* be identified the fallback function passed in at the constructor gets called and its return value becomes
* the return value of this method.
*
* @return a unique result, or the result of the callback provided in the constructor.
*/
T uniqueResult() {
T uniqueResult = findUniqueResult();
return uniqueResult != null ? uniqueResult : fallback.apply(collection);
}
/**
* Filters the collection with the predicate if there are still more then one elements in the collection.
*
* @param predicate To be used for filtering.
*/
SelectionSet<T> filterIfNecessary(Predicate<T> predicate) {
if (findUniqueResult() != null) {
return this;
}
List<T> fillteredList = collection.stream().filter(predicate).collect(Collectors.toList());
return new SelectionSet<T>(fillteredList, fallback);
}
private static <S> Function<Collection<S>, S> defaultFallback() {
return c -> {
if (c.isEmpty()) {
return null;
} else {
throw new IllegalStateException("More then one element in collection.");
}
};
}
private T findUniqueResult() {
return collection.size() == 1 ? collection.iterator().next() : null;
}
}

View File

@@ -37,6 +37,7 @@ import org.w3c.dom.Element;
* @author Thomas Darimont
* @author Christoph Strobl
* @author Peter Rietzler
* @author Jens Schauder
*/
public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSourceSupport {
@@ -63,7 +64,7 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
*/
public XmlRepositoryConfigurationSource(Element element, ParserContext context, Environment environment) {
super(environment);
super(environment, context.getReaderContext().getBeanClassLoader());
Assert.notNull(element, "Element must not be null!");
Assert.notNull(context, "Context must not be null!");
@@ -128,7 +129,7 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
return excludeFilters;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSourceSupport#getIncludeFilters()
*/