DATACMNS-557 - Added support for custom implementations to CDI infrastructure.

The CDI extension now discovers a custom implementation class similarly to the detection when using Spring Data in a Spring container. Extracted the custom implementation detection for re-use. The CDI extension discovers the potentially available bean and hands it to the CdiRepositoryBean for further usage.

Implementations should implement the extended variant of the create(…) method we introduce with this commit to be able to forward the resolved custom implementation bean.

Original pull request: #92.
This commit is contained in:
Mark Paluch
2014-08-07 10:26:10 +02:00
committed by Oliver Gierke
parent 62e66ab305
commit 1f0260e48d
12 changed files with 427 additions and 86 deletions

View File

@@ -43,6 +43,7 @@ import org.springframework.util.StringUtils;
*
* @author Dirk Mahler
* @author Oliver Gierke
* @author Mark Paluch
*/
public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapable {
@@ -50,6 +51,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
private final Set<Annotation> qualifiers;
private final Class<T> repositoryType;
private final Object customImplementation;
private final BeanManager beanManager;
private final String passivationId;
@@ -63,6 +65,18 @@ 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, null);
}
/**
* 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 customImplementation the custom implementation of the {@link org.springframework.data.repository.Repository}, can be {@literal null}.
*/
public CdiRepositoryBean(Set<Annotation> qualifiers, Class<T> repositoryType, BeanManager beanManager, Object customImplementation) {
Assert.notNull(qualifiers);
Assert.notNull(beanManager);
@@ -72,6 +86,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
this.qualifiers = qualifiers;
this.repositoryType = repositoryType;
this.beanManager = beanManager;
this.customImplementation = customImplementation;
this.passivationId = createPassivationId(qualifiers, repositoryType);
}
@@ -162,7 +177,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
creationalContext.release();
}
/*
/*
* (non-Javadoc)
* @see javax.enterprise.inject.spi.Bean#getQualifiers()
*/
@@ -228,7 +243,7 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
return Collections.emptySet();
}
/*
/*
* (non-Javadoc)
* @see javax.enterprise.inject.spi.Bean#getScope()
*/
@@ -251,15 +266,29 @@ public abstract class CdiRepositoryBean<T> implements Bean<T>, PassivationCapabl
* @param repositoryType will never be {@literal null}.
* @return
*/
protected abstract T create(CreationalContext<T> creationalContext, Class<T> repositoryType);
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType) {
return create(creationalContext, repositoryType, customImplementation);
}
/*
/**
* Creates the actual component instance.
*
* @param creationalContext will never be {@literal null}.
* @param repositoryType will never be {@literal null}.
* @param customImplementation can be {@literal null}.
* @return
*/
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType, Object customImplementation) {
throw new UnsupportedOperationException("You have to implement create(CreationalContext<T>, Class<T>, Object) " +
"in order to use custom repository implementations");
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String
.format("JpaRepositoryBean: type='%s', qualifiers=%s", repositoryType.getName(), qualifiers.toString());
return String.format("JpaRepositoryBean: type='%s', qualifiers=%s", repositoryType.getName(), qualifiers.toString());
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2014 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;
/**
* Interface containing the configurable options for the Spring Data repository subsystem using CDI.
*
* @author Mark Paluch
*/
public interface CdiRepositoryConfigurationSource {
/**
* Returns the configured postfix to be used for looking up custom implementation classes.
*
* @return the postfix to use or {@literal null} in case none is configured.
*/
String getRepositoryImplementationPostfix();
}

View File

@@ -15,36 +15,42 @@
*/
package org.springframework.data.repository.cdi;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import javax.enterprise.inject.spi.*;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Qualifier;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
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;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.DefaultRepositoryConfiguration;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Base class for {@link Extension} implementations that create instances for Spring Data repositories.
*
* @author Dirk Mahler
* @author Oliver Gierke
* @author Mark Paluch
*/
public abstract class CdiRepositoryExtensionSupport implements Extension {
@@ -52,6 +58,15 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
private final Map<Class<?>, Set<Annotation>> repositoryTypes = new HashMap<Class<?>, Set<Annotation>>();
private final Set<CdiRepositoryBean<?>> eagerRepositories = new HashSet<CdiRepositoryBean<?>>();
private final CustomRepositoryImplementationDetector customImplementationDetector;
protected CdiRepositoryExtensionSupport() {
Environment environment = new StandardEnvironment();
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
customImplementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory,
environment, resourceLoader);
}
/**
* Implementation of a an observer which checks for Spring Data repository types and stores them in
@@ -160,6 +175,89 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
}
}
/**
* Looks up an instance of a {@link CdiRepositoryConfigurationSource}. In case the instance cannot be found within
* the CDI scope, a default configuration is used.
*
* @return an available CdiRepositoryConfigurationSource instance or a default configuration.
*/
protected CdiRepositoryConfigurationSource lookupConfiguration(BeanManager beanManager, Set<Annotation> qualifiers) {
Set<Bean<?>> beans = beanManager.getBeans(CdiRepositoryConfigurationSource.class, getQualifiersArray(qualifiers));
if (beans.isEmpty()) {
// no own defined type since these would be picked up by CDI by default.
return new CdiRepositoryConfigurationSource() {
@Override
public String getRepositoryImplementationPostfix() {
return DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX;
}
};
}
Bean<?> bean = beans.iterator().next();
CreationalContext<?> creationalContext = beanManager.createCreationalContext(bean);
return (CdiRepositoryConfigurationSource) beanManager.getReference(bean, CdiRepositoryConfigurationSource.class, creationalContext);
}
private Annotation[] getQualifiersArray(Set<Annotation> qualifiers) {
return qualifiers.toArray(new Annotation[qualifiers.size()]);
}
/**
* Try to lookup a custom implementation for a {@link org.springframework.data.repository.Repository}.
*
* @param repositoryType
* @param beanManager
* @param qualifiers
* @return the custom implementation instance or null
*/
protected Object findCustomImplementation(Class<?> repositoryType, BeanManager beanManager, Set<Annotation> qualifiers) {
CdiRepositoryConfigurationSource cdiRepositoryConfiguration = lookupConfiguration(beanManager, qualifiers);
Class<?> customImplementationClass = findCustomImplementationClass(repositoryType, cdiRepositoryConfiguration);
if (customImplementationClass != null) {
Set<Bean<?>> beans = beanManager.getBeans(customImplementationClass, getQualifiersArray(qualifiers));
if (!beans.isEmpty()) {
Bean<?> bean = beans.iterator().next();
CreationalContext<?> creationalContext = beanManager.createCreationalContext(bean);
return beanManager.getReference(bean, customImplementationClass, creationalContext);
}
}
return null;
}
/**
* Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can find
* also a custom repo which is inherieted over many levels.
*
* @param repositoryType The class representing the repository.
* @param cdiRepositoryConfiguration The configuration for CDI usage.
* @return the interface class or null.
*/
protected Class<?> findCustomImplementationClass(Class<?> repositoryType, CdiRepositoryConfigurationSource cdiRepositoryConfiguration) {
String className = constructCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
AbstractBeanDefinition beanDefinition = customImplementationDetector.detectCustomImplementation(className,
Collections.singleton(repositoryType.getPackage().getName()));
if (beanDefinition != null) {
try {
return Class.forName(beanDefinition.getBeanClassName());
} catch (ClassNotFoundException e) {
throw new UnsatisfiedResolutionException(String.format("Unable to resolve class for '%s'",
beanDefinition.getBeanClassName()), e);
}
}
return null;
}
private String constructCustomImplementationClassName(Class<?> repositoryType, CdiRepositoryConfigurationSource cdiRepositoryConfiguration) {
String configuredPostfix = cdiRepositoryConfiguration.getRepositoryImplementationPostfix();
String implementationPostfix = StringUtils.hasText(configuredPostfix) ? configuredPostfix : DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX;
return ClassUtils.getShortName(repositoryType) + implementationPostfix;
}
@SuppressWarnings("all")
static class DefaultAnnotationLiteral extends AnnotationLiteral<Default> implements Default {

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2014 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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
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.RegexPatternTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository}
*
* @author Mark Paluch
*/
public class CustomRepositoryImplementationDetector {
private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/*%s.class";
private MetadataReaderFactory metadataReaderFactory;
private Environment environment;
private ResourceLoader resourceLoader;
/**
*
* Creates a new {@link CustomRepositoryImplementationDetector} from the given {@link org.springframework.core.type.classreading.MetadataReaderFactory},
* {@link org.springframework.core.env.Environment} and {@link org.springframework.core.io.ResourceLoader}.
*
* @param metadataReaderFactory must not be {@literal null}.
* @param environment must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
*/
public CustomRepositoryImplementationDetector(MetadataReaderFactory metadataReaderFactory, Environment environment,
ResourceLoader resourceLoader) {
Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
Assert.notNull(environment, "Environment must not be null!");
this.metadataReaderFactory = metadataReaderFactory;
this.environment = environment;
this.resourceLoader = resourceLoader;
}
/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param className must not be {@literal null}.
* @param basePackages must not be {@literal null}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found
*/
public AbstractBeanDefinition detectCustomImplementation(String className, Iterable<String> basePackages) {
Assert.notNull(className, "ClassName must not be null!");
Assert.notNull(basePackages, "BasePackages must not be null!");
// Build pattern to lookup implementation class
Pattern pattern = Pattern.compile(".*\\." + className);
// Build classpath scanner and lookup bean definition
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.setEnvironment(environment);
provider.setResourceLoader(resourceLoader);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, className));
provider.setMetadataReaderFactory(metadataReaderFactory);
provider.addIncludeFilter(new RegexPatternTypeFilter(pattern));
Set<BeanDefinition> definitions = new HashSet<BeanDefinition>();
for (String basePackage : basePackages) {
definitions.addAll(provider.findCandidateComponents(basePackage));
}
if (definitions.isEmpty()) {
return null;
}
if (definitions.size() == 1) {
return (AbstractBeanDefinition) definitions.iterator().next();
}
List<String> implementationClassNames = new ArrayList<String>();
for (BeanDefinition bean : definitions) {
implementationClassNames.add(bean.getBeanClassName());
}
throw new IllegalStateException(String.format(
"Ambiguous custom implementations detected! Found %s but expected a single implementation!",
StringUtils.collectionToCommaDelimitedString(implementationClassNames)));
}
}

View File

@@ -29,8 +29,8 @@ import org.springframework.util.StringUtils;
public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSource> implements
RepositoryConfiguration<T> {
public static final String DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX = "Impl";
private static final Key DEFAULT_QUERY_LOOKUP_STRATEGY = Key.CREATE_IF_NOT_FOUND;
private static final String DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX = "Impl";
private final T configurationSource;
private final BeanDefinition definition;

View File

@@ -15,25 +15,16 @@
*/
package org.springframework.data.repository.config;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -46,14 +37,13 @@ import org.springframework.util.StringUtils;
class RepositoryBeanDefinitionBuilder {
private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryBeanDefinitionBuilder.class);
private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/*%s.class";
private final BeanDefinitionRegistry registry;
private final RepositoryConfigurationExtension extension;
private final ResourceLoader resourceLoader;
private final Environment environment;
private final MetadataReaderFactory metadataReaderFactory;
private CustomRepositoryImplementationDetector implementationDetector;
/**
* Creates a new {@link RepositoryBeanDefinitionBuilder} from the given {@link BeanDefinitionRegistry},
@@ -69,21 +59,20 @@ class RepositoryBeanDefinitionBuilder {
Assert.notNull(extension, "RepositoryConfigurationExtension must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
Assert.notNull(environment, "Environement must not be null!");
Assert.notNull(environment, "Environment must not be null!");
this.registry = registry;
this.extension = extension;
this.resourceLoader = resourceLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.environment = environment;
this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader);
}
/**
* Builds a new {@link BeanDefinitionBuilder} from the given {@link BeanDefinitionRegistry} and {@link ResourceLoader}
* .
*
* @param registry must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
* @param configuration must not be {@literal null}.
* @return
*/
public BeanDefinitionBuilder build(RepositoryConfiguration<?> configuration) {
@@ -136,7 +125,8 @@ class RepositoryBeanDefinitionBuilder {
return beanName;
}
AbstractBeanDefinition beanDefinition = detectCustomImplementation(configuration);
AbstractBeanDefinition beanDefinition = implementationDetector.detectCustomImplementation(
configuration.getImplementationClassName(), configuration.getBasePackages());
if (null == beanDefinition) {
return null;
@@ -154,47 +144,5 @@ class RepositoryBeanDefinitionBuilder {
return beanName;
}
/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param config must not be {@literal null}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found
*/
private AbstractBeanDefinition detectCustomImplementation(RepositoryConfiguration<?> configuration) {
// Build pattern to lookup implementation class
String className = configuration.getImplementationClassName();
Pattern pattern = Pattern.compile(".*\\." + className);
// Build classpath scanner and lookup bean definition
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.setEnvironment(environment);
provider.setResourceLoader(resourceLoader);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, className));
provider.setMetadataReaderFactory(metadataReaderFactory);
provider.addIncludeFilter(new RegexPatternTypeFilter(pattern));
Set<BeanDefinition> definitions = new HashSet<BeanDefinition>();
for (String basePackage : configuration.getBasePackages()) {
definitions.addAll(provider.findCandidateComponents(basePackage));
}
if (definitions.isEmpty()) {
return null;
}
if (definitions.size() == 1) {
return (AbstractBeanDefinition) definitions.iterator().next();
}
List<String> implementationClassNames = new ArrayList<String>();
for (BeanDefinition bean : definitions) {
implementationClassNames.add(bean.getBeanClassName());
}
throw new IllegalStateException(String.format(
"Ambiguous custom implementations detected! Found %s but expected a single implementation!",
StringUtils.collectionToCommaDelimitedString(implementationClassNames)));
}
}