From 81af5ee730dc7473fe67dd34995c836e29a87cd2 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 28 Jul 2014 19:42:10 +0200 Subject: [PATCH] DATACMNS-526 - Improved configuration behavior in multi-store scenarios. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now apply a stricter repository interface selection if we detect multiple Spring Data modules to be in the classpath to avoid the repository scanning accidentally overriding each others definitions or even picking up interfaces they weren't intended to manage. The detection is based on a type scan in dedicated base package where subtypes of RepositoryFactorySupport usually reside. If more than one type is found, we activate strict scanning. The strict check is actually implemented in RepositoryConfigurationExtensionSupport to be accessible for store implementations. By default we try to load the repository candidate interface and inspect the managed domain types for a collection of annotations (see RepositoryConfigurationExtensionSupport.getIdentifyingAnnotations()). Implementors still have the chance to customize the behavior by overriding isStrictRepositoryCandidate(…). We also introduced a getModuleName() to be able to create better logging output in terms of repository registration. Moved registration of RepositoryConfigurationExtension as Spring bean into the base types for XML and annotation configuration support to make sure they only kick in if explicit configuration is used. --- .../data/config/TypeFilterParser.java | 4 +- ...notationRepositoryConfigurationSource.java | 29 ++++ .../RepositoryBeanDefinitionParser.java | 5 +- ...ositoryBeanDefinitionRegistrarSupport.java | 6 +- .../RepositoryConfigurationDelegate.java | 85 ++++++++---- .../RepositoryConfigurationExtension.java | 26 +++- ...positoryConfigurationExtensionSupport.java | 124 ++++++++++++++++-- .../config/RepositoryConfigurationSource.java | 8 ++ .../config/RepositoryConfigurationUtils.java | 65 +++++++++ .../XmlRepositoryConfigurationSource.java | 14 +- .../support/AbstractRepositoryMetadata.java | 16 +++ .../support/RepositoryFactorySupport.java | 5 +- ...epositoryConfigurationSourceUnitTests.java | 19 ++- .../RepositoryComponentProviderUnitTests.java | 4 +- ...onfigurationExtensionSupportUnitTests.java | 93 +++++++++++++ 15 files changed, 455 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/config/RepositoryConfigurationUtils.java create mode 100644 src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java diff --git a/src/main/java/org/springframework/data/config/TypeFilterParser.java b/src/main/java/org/springframework/data/config/TypeFilterParser.java index 340b88038..c7a06ff77 100644 --- a/src/main/java/org/springframework/data/config/TypeFilterParser.java +++ b/src/main/java/org/springframework/data/config/TypeFilterParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2012 the original author or authors. + * Copyright 2010-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. @@ -82,7 +82,7 @@ public class TypeFilterParser { * @param type must not be {@literal null}. * @return */ - public Iterable parseTypeFilters(Element element, Type type) { + public Collection parseTypeFilters(Element element, Type type) { NodeList nodeList = element.getChildNodes(); Collection filters = new HashSet(); diff --git a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java index 411a9dbbf..df5f704a5 100644 --- a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java @@ -58,6 +58,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura private final AnnotationMetadata metadata; private final AnnotationAttributes attributes; private final ResourceLoader resourceLoader; + private final boolean hasExplicitFilters; /** * Creates a new {@link AnnotationRepositoryConfigurationSource} from the given {@link AnnotationMetadata} and @@ -80,6 +81,25 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura this.attributes = new AnnotationAttributes(metadata.getAnnotationAttributes(annotation.getName())); this.metadata = metadata; this.resourceLoader = resourceLoader; + this.hasExplicitFilters = hasExplicitFilters(attributes); + } + + /** + * Returns whether there's explicit configuration of include- or exclude filters. + * + * @param attributes must not be {@literal null}. + * @return + */ + private static boolean hasExplicitFilters(AnnotationAttributes attributes) { + + for (String attribute : Arrays.asList("includeFilters", "excludeFilters")) { + + if (attributes.getAnnotationArray(attribute).length > 0) { + return true; + } + } + + return false; } /* @@ -268,6 +288,15 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura return StringUtils.hasText(attribute) ? attribute : null; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationSource#usesExplicitFilters() + */ + @Override + public boolean usesExplicitFilters() { + return hasExplicitFilters; + } + /** * Safely reads the {@code pattern} attribute from the given {@link AnnotationAttributes} and returns an empty list if * the attribute is not present. diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionParser.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionParser.java index 6724dedc4..681bbab9d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionParser.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionParser.java @@ -60,12 +60,15 @@ public class RepositoryBeanDefinitionParser implements BeanDefinitionParser { Environment environment = parser.getDelegate().getEnvironment(); ResourceLoader resourceLoader = parser.getReaderContext().getResourceLoader(); + BeanDefinitionRegistry registry = parser.getRegistry(); XmlRepositoryConfigurationSource configSource = new XmlRepositoryConfigurationSource(element, parser, environment); RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configSource, resourceLoader, environment); - for (BeanComponentDefinition definition : delegate.registerRepositoriesIn(parser.getRegistry(), extension)) { + RepositoryConfigurationUtils.exposeRegistration(extension, registry, configSource); + + for (BeanComponentDefinition definition : delegate.registerRepositoriesIn(registry, extension)) { parser.getReaderContext().fireComponentRegistered(definition); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupport.java index 6a8fe16b7..183d75d6b 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupport.java @@ -74,9 +74,13 @@ public abstract class RepositoryBeanDefinitionRegistrarSupport implements Import AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource( annotationMetadata, getAnnotation(), resourceLoader, environment); + RepositoryConfigurationExtension extension = getExtension(); + RepositoryConfigurationUtils.exposeRegistration(extension, registry, configurationSource); + RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, resourceLoader, environment); - delegate.registerRepositoriesIn(registry, getExtension()); + + delegate.registerRepositoriesIn(registry, extension); } /** 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 8d7e307ee..237f37c6f 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -15,8 +15,7 @@ */ package org.springframework.data.repository.config; -import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; - +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -27,11 +26,15 @@ 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.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.util.Assert; /** @@ -46,11 +49,16 @@ public class RepositoryConfigurationDelegate { private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryConfigurationDelegate.class); + private static final String REPOSITORY_REGISTRATION = "Spring Data {} - Registering repository: {} - Interface: {} - Factory: {}"; + private static final String MULTIPLE_MODULES = "Multiple Spring Data modules found, entering strict repository configuration mode!"; + private static final String MODULE_DETECTION_PACKAGE = "org.springframework.data.**.repository.support"; + private final RepositoryConfigurationSource configurationSource; private final ResourceLoader resourceLoader; private final Environment environment; private final BeanNameGenerator generator; private final boolean isXml; + private final boolean inMultiStoreMode; /** * Creates a new {@link RepositoryConfigurationDelegate} for the given {@link RepositoryConfigurationSource} and @@ -77,6 +85,7 @@ public class RepositoryConfigurationDelegate { this.configurationSource = configurationSource; this.resourceLoader = resourceLoader; this.environment = defaultEnvironment(environment, resourceLoader); + this.inMultiStoreMode = multipleStoresDetected(); } /** @@ -107,8 +116,6 @@ public class RepositoryConfigurationDelegate { public List registerRepositoriesIn(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension) { - exposeRegistration(extension, registry); - extension.registerBeansForRoot(registry, configurationSource); RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, resourceLoader, @@ -116,7 +123,7 @@ public class RepositoryConfigurationDelegate { List definitions = new ArrayList(); for (RepositoryConfiguration configuration : extension - .getRepositoryConfigurations(configurationSource, resourceLoader)) { + .getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode)) { BeanDefinitionBuilder definitionBuilder = builder.build(configuration); @@ -132,8 +139,8 @@ public class RepositoryConfigurationDelegate { String beanName = generator.generateBeanName(beanDefinition, registry); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Registering repository: " + beanName + " - Interface: " + configuration.getRepositoryInterface() - + " - Factory: " + extension.getRepositoryFactoryClassName()); + LOGGER.debug(REPOSITORY_REGISTRATION, extension.getModuleName(), beanName, + configuration.getRepositoryInterface(), extension.getRepositoryFactoryClassName()); } registry.registerBeanDefinition(beanName, beanDefinition); @@ -144,29 +151,57 @@ public class RepositoryConfigurationDelegate { } /** - * Registeres the given {@link RepositoryConfigurationExtension} to indicate the repository configuration for a - * particular store (expressed through the extension's concrete type) has appened. Useful for downstream components - * that need to detect exactly that case. The bean definition is marked as lazy-init so that it doesn't get - * instantiated if no one really cares. + * Scans {@code repository.support} packages for implementations of {@link RepositoryFactorySupport}. Finding more + * than a single type is considered a multi-store configuration scenario which will trigger stricter repository + * scanning. * - * @param extension - * @param registry + * @return */ - private void exposeRegistration(RepositoryConfigurationExtension extension, BeanDefinitionRegistry registry) { + private boolean multipleStoresDetected() { - Class extensionType = extension.getClass(); - String beanName = extensionType.getName().concat(GENERATED_BEAN_NAME_SEPARATOR).concat("0"); + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, + environment); + scanner.addIncludeFilter(new LenientAssignableTypeFilter(RepositoryFactorySupport.class)); + int numberOfModulesFound = scanner.findCandidateComponents(MODULE_DETECTION_PACKAGE).size(); - if (registry.containsBeanDefinition(beanName)) { - return; + if (numberOfModulesFound > 1) { + LOGGER.debug(MULTIPLE_MODULES); + return true; } - // Register extension as bean to indicate repository parsing and registration has happened - RootBeanDefinition definition = new RootBeanDefinition(extensionType); - definition.setSource(configurationSource.getSource()); - definition.setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); - definition.setLazyInit(true); + return false; + } - registry.registerBeanDefinition(beanName, definition); + /** + * Special {@link AssignableTypeFilter} that generally considers exceptions during type matching indicating a + * non-match. TODO: Remove after upgrade to Spring 4.0.7. + * + * @see https://jira.spring.io/browse/SPR-12042 + * @author Oliver Gierke + */ + private static class LenientAssignableTypeFilter extends AssignableTypeFilter { + + /** + * Creates a new {@link LenientAssignableTypeFilter} for the given target type. + * + * @param targetType must not be {@literal null}. + */ + public LenientAssignableTypeFilter(Class targetType) { + super(targetType); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { + + try { + return super.match(metadataReader, metadataReaderFactory); + } catch (Exception o_O) { + return false; + } + } } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java index b1c505805..b30a60ea7 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-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. @@ -30,16 +30,40 @@ import org.springframework.core.io.ResourceLoader; */ public interface RepositoryConfigurationExtension { + /** + * Returns the descriptive name of the module. + * + * @return + */ + String getModuleName(); + /** * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. * * @param configSource must not be {@literal null}. * @param loader must not be {@literal null}. + * @deprecated call or implement + * {@link #getRepositoryConfigurations(RepositoryConfigurationSource, ResourceLoader, boolean)} instead. * @return */ + @Deprecated Collection> getRepositoryConfigurations( T configSource, ResourceLoader loader); + /** + * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. + * + * @param configSource + * @param loader + * @param strictMatchesOnly whether to return strict repository matches only. Handing in {@literal true} will cause + * the repository interfaces and domain types handled to be checked whether they are managed by the current + * store. + * @return + * @since 1.9 + */ + Collection> getRepositoryConfigurations( + T configSource, ResourceLoader loader, boolean strictMatchesOnly); + /** * Returns the default location of the Spring Data named queries. * diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java index b22833c4e..1dc2a63ad 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -17,16 +17,24 @@ package org.springframework.data.repository.config; import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; +import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; +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.core.annotation.AnnotationUtils; import org.springframework.core.io.ResourceLoader; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Base implementation of {@link RepositoryConfigurationExtension} to ease the implementation of the interface. Will @@ -37,14 +45,37 @@ import org.springframework.util.Assert; */ public abstract class RepositoryConfigurationExtensionSupport implements RepositoryConfigurationExtension { + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryConfigurationExtensionSupport.class); + private static final String CLASS_LOADING_ERROR = "%s - Could not load type %s using class loader %s."; + private static final String MULTI_STORE_DROPPED = "Spring Data {} - Could not safely identify store assignment for repository candidate {}."; + protected static final String REPOSITORY_INTERFACE_POST_PROCESSOR = "org.springframework.data.repository.core.support.RepositoryInterfaceAwareBeanPostProcessor"; + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() + */ + @Override + public String getModuleName() { + return StringUtils.capitalize(getModulePrefix()); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryConfigurations(org.springframework.data.repository.config.RepositoryConfigurationSource, org.springframework.core.io.ResourceLoader) */ public Collection> getRepositoryConfigurations( T configSource, ResourceLoader loader) { + return getRepositoryConfigurations(configSource, loader, false); + } + + /* + * + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryConfigurations(org.springframework.data.repository.config.RepositoryConfigurationSource, org.springframework.core.io.ResourceLoader, boolean) + */ + public Collection> getRepositoryConfigurations( + T configSource, ResourceLoader loader, boolean strictMatchesOnly) { Assert.notNull(configSource); Assert.notNull(loader); @@ -52,8 +83,21 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit Set> result = new HashSet>(); for (BeanDefinition candidate : configSource.getCandidates(loader)) { - result.add(getRepositoryConfiguration(candidate, configSource)); + + RepositoryConfiguration configuration = getRepositoryConfiguration(candidate, configSource); + + if (!strictMatchesOnly || configSource.usesExplicitFilters()) { + result.add(configuration); + continue; + } + + Class repositoryInterface = loadRepositoryInterface(configuration, loader); + + if (repositoryInterface == null || isStrictRepositoryCandidate(repositoryInterface)) { + result.add(configuration); + } } + return result; } @@ -88,24 +132,29 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) */ - public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { - - } + public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {} /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) */ - public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { - - } + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {} /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource) */ - public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) { + public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) {} + /** + * Return the annotations to scan domain types for when evaluating repository interfaces for store assignment. Modules + * should return the annotations that identify a domain type as managed by the store explicitly. + * + * @return + * @since 1.9 + */ + protected Collection> getIdentifyingAnnotations() { + return Collections.emptySet(); } /** @@ -155,4 +204,61 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit BeanDefinition definition, T configSource) { return new DefaultRepositoryConfiguration(configSource, definition); } + + /** + * Returns whether the given repository interface is a candidate for bean definition creation in the strict repository + * detection mode. The default implementation inspects the domain type managed for a set of well-known annotations + * (see {@link #getIdentifyingAnnotations()}). If none of them is found, the candidate is discarded. Implementations + * should make sure, the only return {@literal true} if they're really sure the interface handed to the method is + * really a store interface. + * + * @param repositoryInterface + * @return + * @since 1.9 + */ + protected boolean isStrictRepositoryCandidate(Class repositoryInterface) { + + RepositoryMetadata metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + Class domainType = metadata.getDomainType(); + + Collection> annotations = getIdentifyingAnnotations(); + + if (annotations.isEmpty()) { + return true; + } + + for (Class annotationType : annotations) { + if (AnnotationUtils.findAnnotation(domainType, annotationType) != null) { + return true; + } + } + + LOGGER.debug(MULTI_STORE_DROPPED, getModuleName(), repositoryInterface); + + return false; + } + + /** + * Loads the repository interface contained in the given {@link RepositoryConfiguration} using the given + * {@link ResourceLoader}. + * + * @param configuration must not be {@literal null}. + * @param loader must not be {@literal null}. + * @return the repository interface or {@literal null} if it can't be loaded. + */ + private static Class loadRepositoryInterface(RepositoryConfiguration configuration, ResourceLoader loader) { + + String repositoryInterface = configuration.getRepositoryInterface(); + ClassLoader classLoader = loader.getClassLoader(); + + try { + return org.springframework.util.ClassUtils.forName(repositoryInterface, classLoader); + } catch (ClassNotFoundException e) { + LOGGER.warn(String.format(CLASS_LOADING_ERROR, repositoryInterface, classLoader), e); + } catch (LinkageError e) { + LOGGER.warn(String.format(CLASS_LOADING_ERROR, repositoryInterface, classLoader), e); + } + + return null; + } } 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 82b9ad2b1..612cd33fd 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationSource.java @@ -88,4 +88,12 @@ public interface RepositoryConfigurationSource { * @since 1.8 */ String getAttribute(String name); + + /** + * Returns whether the configuration uses explicit filtering to scan for repository types. + * + * @return whether the configuration uses explicit filtering to scan for repository types. + * @since 1.9 + */ + boolean usesExplicitFilters(); } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationUtils.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationUtils.java new file mode 100644 index 000000000..58b40c288 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationUtils.java @@ -0,0 +1,65 @@ +/* + * 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 static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.Assert; + +/** + * Helper class to centralize common functionality that needs to be used in various places of the configuration + * implementation. + * + * @author Oliver Gierke + */ +public class RepositoryConfigurationUtils { + + /** + * Registeres the given {@link RepositoryConfigurationExtension} to indicate the repository configuration for a + * particular store (expressed through the extension's concrete type) has appened. Useful for downstream components + * that need to detect exactly that case. The bean definition is marked as lazy-init so that it doesn't get + * instantiated if no one really cares. + * + * @param extension must not be {@literal null}. + * @param registry must not be {@literal null}. + * @param configurationSource must not be {@literal null}. + */ + public static void exposeRegistration(RepositoryConfigurationExtension extension, BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + + Assert.notNull(extension, "RepositoryConfigurationExtension must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + Assert.notNull(configurationSource, "RepositoryConfigurationSource must not be null!"); + + Class extensionType = extension.getClass(); + String beanName = extensionType.getName().concat(GENERATED_BEAN_NAME_SEPARATOR).concat("0"); + + if (registry.containsBeanDefinition(beanName)) { + return; + } + + // Register extension as bean to indicate repository parsing and registration has happened + RootBeanDefinition definition = new RootBeanDefinition(extensionType); + definition.setSource(configurationSource.getSource()); + definition.setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); + definition.setLazyInit(true); + + registry.registerBeanDefinition(beanName, definition); + } +} diff --git a/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java index 0318a33fe..2f1f0bc4c 100644 --- a/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/XmlRepositoryConfigurationSource.java @@ -16,6 +16,7 @@ package org.springframework.data.repository.config; import java.util.Arrays; +import java.util.Collection; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.core.env.Environment; @@ -46,8 +47,8 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou private final Element element; private final ParserContext context; - private final Iterable includeFilters; - private final Iterable excludeFilters; + private final Collection includeFilters; + private final Collection excludeFilters; /** * Creates a new {@link XmlRepositoryConfigurationSource} using the given {@link Element} and {@link ParserContext}. @@ -175,4 +176,13 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou return StringUtils.hasText(attribute) ? attribute : null; } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationSource#usesExplicitFilters() + */ + @Override + public boolean usesExplicitFilters() { + return !(this.includeFilters.isEmpty() && this.excludeFilters.isEmpty()); + } } diff --git a/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java b/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java index ab4136a01..9f0d849f3 100644 --- a/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.CrudMethods; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.util.QueryExecutionConverters; @@ -52,6 +53,21 @@ public abstract class AbstractRepositoryMetadata implements RepositoryMetadata { this.typeInformation = ClassTypeInformation.from(repositoryInterface); } + /** + * Creates a new {@link RepositoryMetadata} for the given repsository interface. + * + * @param repositoryInterface must not be {@literal null}. + * @since 1.9 + * @return + */ + public static RepositoryMetadata getMetadata(Class repositoryInterface) { + + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + + return Repository.class.isAssignableFrom(repositoryInterface) ? new DefaultRepositoryMetadata(repositoryInterface) + : new AnnotationRepositoryMetadata(repositoryInterface); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(java.lang.reflect.Method) diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index a164e053e..19a1a5ee5 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2013 the original author or authors. + * Copyright 2008-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. @@ -201,8 +201,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware { * @return */ RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { - return Repository.class.isAssignableFrom(repositoryInterface) ? new DefaultRepositoryMetadata(repositoryInterface) - : new AnnotationRepositoryMetadata(repositoryInterface); + return AbstractRepositoryMetadata.getMetadata(repositoryInterface); } /** diff --git a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java index c4d2a3508..94a719536 100644 --- a/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSourceUnitTests.java @@ -23,6 +23,8 @@ import java.util.Collection; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; @@ -127,16 +129,26 @@ public class AnnotationRepositoryConfigurationSourceUnitTests { public void returnsEmptyStringForBasePackage() throws Exception { StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(getClass().getClassLoader().loadClass( - "TypeInDefaultPackage")); + "TypeInDefaultPackage"), true); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, EnableRepositories.class, resourceLoader, environment); assertThat(configurationSource.getBasePackages(), hasItem("")); } + /** + * @see DATACMNS-526 + */ + @Test + public void detectsExplicitFilterConfiguration() { + + assertThat(getConfigSource(ConfigurationWithExplicitFilter.class).usesExplicitFilters(), is(true)); + assertThat(getConfigSource(DefaultConfiguration.class).usesExplicitFilters(), is(false)); + } + private AnnotationRepositoryConfigurationSource getConfigSource(Class type) { - AnnotationMetadata metadata = new StandardAnnotationMetadata(type); + AnnotationMetadata metadata = new StandardAnnotationMetadata(type, true); return new AnnotationRepositoryConfigurationSource(metadata, EnableRepositories.class, resourceLoader, environment); } @@ -150,4 +162,7 @@ public class AnnotationRepositoryConfigurationSourceUnitTests { @EnableRepositories(considerNestedRepositories = true) static class DefaultConfigurationWithNestedRepositories {} + + @EnableRepositories(excludeFilters = { @Filter(Primary.class) }) + static class ConfigurationWithExplicitFilter {} } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java index 234a4a881..3a866218a 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryComponentProviderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -74,7 +74,7 @@ public class RepositoryComponentProviderUnitTests { Set components = provider.findCandidateComponents("org.springframework.data.repository.config"); String nestedRepositoryClassName = "org.springframework.data.repository.config.RepositoryComponentProviderUnitTests$MyNestedRepository"; - assertThat(components.size(), is(3)); + assertThat(components.size(), is(greaterThanOrEqualTo(1))); assertThat(components, Matchers. hasItem(hasProperty("beanClassName", is(nestedRepositoryClassName)))); } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java new file mode 100644 index 000000000..423202e7c --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java @@ -0,0 +1,93 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.context.annotation.Primary; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +/** + * Unit tests for {@link RepositoryConfigurationExtensionSupport}. + * + * @author Oliver Gierke + */ +public class RepositoryConfigurationExtensionSupportUnitTests { + + RepositoryConfigurationExtensionSupport extension = new SampleRepositoryConfigurationExtension(); + + /** + * @see DATACMNS-526 + */ + @Test + public void doesNotConsiderRepositoryForPlainTypeStrictMatch() { + assertThat(extension.isStrictRepositoryCandidate(PlainTypeRepository.class), is(false)); + } + + /** + * @see DATACMNS-526 + */ + @Test + public void considersRepositoryWithAnnotatedTypeStrictMatch() { + assertThat(extension.isStrictRepositoryCandidate(AnnotatedTypeRepository.class), is(true)); + } + + static class SampleRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return "core"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryClassName() + */ + @Override + public String getRepositoryFactoryClassName() { + return RepositoryFactorySupport.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations() + */ + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.> singleton(Primary.class); + } + } + + @Primary + static class AnnotatedType {} + + static class PlainType {} + + interface AnnotatedTypeRepository extends Repository {} + + interface PlainTypeRepository extends Repository {} +}