From 7c7fdb07363791fb3c72f4946839f4c166196ebe Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 21 Feb 2013 16:47:00 -0800 Subject: [PATCH] Add support for DeferredImportSelector Add DeferredImportSelector interface that can be used to select imports after all @Configuration beans have been processed. --- .../annotation/ConfigurationClassParser.java | 78 +++++++++- .../ConfigurationClassPostProcessor.java | 15 +- .../annotation/DeferredImportSelector.java | 33 ++++ .../context/annotation/ImportSelector.java | 6 + .../annotation/ImportSelectorTests.java | 146 ++++++++++++++++++ 5 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 93731a0025..5b1736fd6f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -25,6 +25,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; @@ -32,19 +34,23 @@ import java.util.Stack; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.NestedIOException; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; @@ -82,6 +88,15 @@ import static org.springframework.context.annotation.MetadataUtils.*; */ class ConfigurationClassParser { + private static final Comparator DEFERRED_IMPORT_COMPARATOR = + new Comparator() { + public int compare(DeferredImportSelectorHolder o1, + DeferredImportSelectorHolder o2) { + return AnnotationAwareOrderComparator.INSTANCE.compare( + o1.getImportSelector(), o2.getImportSelector()); + } + }; + private final MetadataReaderFactory metadataReaderFactory; private final ProblemReporter problemReporter; @@ -106,6 +121,8 @@ class ConfigurationClassParser { private final BeanNameGenerator beanNameGenerator; + private final List deferredImportSelectors = + new LinkedList(); /** * Create a new {@link ConfigurationClassParser} instance that will be used @@ -125,6 +142,23 @@ class ConfigurationClassParser { resourceLoader, environment, componentScanBeanNameGenerator, registry); } + public void parse(Set configCandidates) { + for (BeanDefinitionHolder holder : configCandidates) { + BeanDefinition bd = holder.getBeanDefinition(); + try { + if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { + parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); + } + else { + parse(bd.getBeanClassName(), holder.getBeanName()); + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); + } + } + processDeferredImportSelectors(); + } /** * Parse the specified {@link Configuration @Configuration} class. @@ -142,7 +176,7 @@ class ConfigurationClassParser { * @param clazz the Class to parse * @param beanName must not be null (as of Spring 3.1.1) */ - public void parse(Class clazz, String beanName) throws IOException { + protected void parse(Class clazz, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(clazz, beanName)); } @@ -369,6 +403,21 @@ class ConfigurationClassParser { } } + private void processDeferredImportSelectors() { + Collections.sort(this.deferredImportSelectors, DEFERRED_IMPORT_COMPARATOR); + for (DeferredImportSelectorHolder deferredImport : this.deferredImportSelectors) { + try { + ConfigurationClass configClass = deferredImport.getConfigurationClass(); + String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata()); + processImport(configClass, Arrays.asList(imports), false); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Failed to load bean class: ", ex); + } + } + deferredImportSelectors.clear(); + } + private void processImport(ConfigurationClass configClass, Collection classesToImport, boolean checkForCircularImports) throws IOException { if (checkForCircularImports && this.importStack.contains(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack, configClass.getMetadata())); @@ -384,7 +433,12 @@ class ConfigurationClassParser { Class candidateClass = (candidate instanceof Class ? (Class) candidate : this.resourceLoader.getClassLoader().loadClass((String) candidate)); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); invokeAwareMethods(selector); - processImport(configClass, Arrays.asList(selector.selectImports(importingClassMetadata)), false); + if(selector instanceof DeferredImportSelector) { + this.deferredImportSelectors.add(new DeferredImportSelectorHolder( + configClass, (DeferredImportSelector) selector)); + } else { + processImport(configClass, Arrays.asList(selector.selectImports(importingClassMetadata)), false); + } } else if (checkAssignability(ImportBeanDefinitionRegistrar.class, candidateToCheck)) { // the candidate class is an ImportBeanDefinitionRegistrar -> delegate to it to register additional bean definitions @@ -539,4 +593,24 @@ class ConfigurationClassParser { } } + + private static class DeferredImportSelectorHolder { + + private ConfigurationClass configurationClass; + + private DeferredImportSelector importSelector; + + public DeferredImportSelectorHolder(ConfigurationClass configurationClass, DeferredImportSelector importSelector) { + this.configurationClass = configurationClass; + this.importSelector = importSelector; + } + + public ConfigurationClass getConfigurationClass() { + return configurationClass; + } + + public DeferredImportSelector getImportSelector() { + return importSelector; + } + } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 933192970a..dbd94f3310 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -276,20 +276,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); - for (BeanDefinitionHolder holder : configCandidates) { - BeanDefinition bd = holder.getBeanDefinition(); - try { - if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { - parser.parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); - } - else { - parser.parse(bd.getBeanClassName(), holder.getBeanName()); - } - } - catch (IOException ex) { - throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); - } - } + parser.parse(configCandidates); parser.validate(); // Handle any @PropertySource annotations diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java new file mode 100644 index 0000000000..2c104126f9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 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.context.annotation; + +/** + * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans + * have been processed. This type of selector can be particularly useful when the selected + * imports are {@code @Conditional}. + * + *

Implementations can also extend the {@link org.springframework.core.Ordered} + * interface or use the {@link org.springframework.core.annotation.Order} annotation to + * indicate a precedence against other {@link DeferredImportSelector}s. + * + * @author Phillip Webb + * @since 4.0 + */ +public interface DeferredImportSelector extends ImportSelector { + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index 5fcb9b960f..5d8848001d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -32,8 +32,14 @@ import org.springframework.core.type.AnnotationMetadata; *

  • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
  • * * + *

    ImportSelectors are usually processed in the same way as regular {@code @Import} + * annotations, however, it is also possible to defer selection of imports until all + * {@code @Configuration} classes have been processed (see {@link DeferredImportSelector} + * for details). + * * @author Chris Beams * @since 3.1 + * @see DeferredImportSelector * @see Import * @see ImportBeanDefinitionRegistrar * @see Configuration diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java new file mode 100644 index 0000000000..abdfcdbf6d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2013 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.context.annotation; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; +import org.mockito.InOrder; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Tests for {@link ImportSelector} and {@link DeferredImportSelector}. + * + * @author Phillip Webb + */ +public class ImportSelectorTests { + + @Test + public void importSelectors() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + beanFactory); + context.register(Config.class); + context.refresh(); + context.getBean(Config.class); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any(BeanDefinition.class)); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any(BeanDefinition.class)); + } + + @Sample + @Configuration + static class Config { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import({ DeferredImportSelector1.class, DeferredImportSelector2.class, + ImportSelector1.class, ImportSelector2.class }) + public static @interface Sample { + } + + public static class ImportSelector1 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { ImportedSelector1.class.getName() }; + } + } + + public static class ImportSelector2 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { ImportedSelector2.class.getName() }; + } + } + + public static class DeferredImportSelector1 implements DeferredImportSelector, Ordered { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { DeferredImportedSelector1.class.getName() }; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + public static class DeferredImportSelector2 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { DeferredImportedSelector2.class.getName() }; + } + + } + + @Configuration + public static class ImportedSelector1 { + + @Bean + public String a() { + return "a"; + } + } + + @Configuration + public static class ImportedSelector2 { + + @Bean + public String b() { + return "b"; + } + } + + @Configuration + public static class DeferredImportedSelector1 { + + @Bean + public String c() { + return "c"; + } + } + + @Configuration + public static class DeferredImportedSelector2 { + + @Bean + public String d() { + return "d"; + } + } + +}