From cc12afdea2c4b2c3073c1c2bbc88091a4e7db74e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 8 Mar 2018 23:01:44 -0800 Subject: [PATCH] Add support for deferred import selector group This commit allows several DeferredImportSelector instances to be grouped and managed in a centralized fashion. This typically allows different instances to provide a consistent ordered set of imports to apply. Issue: SPR-16589 --- .../annotation/ConfigurationClassParser.java | 101 +++++++++-- .../annotation/DeferredImportSelector.java | 88 ++++++++- .../DeferredImportSelectorTests.java | 61 +++++++ .../annotation/ImportSelectorTests.java | 171 +++++++++++++++++- 4 files changed, 403 insertions(+), 18 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.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 2e3800e14fa..71790901d92 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 @@ -51,6 +51,7 @@ 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.annotation.ConfigurationCondition.ConfigurationPhase; +import org.springframework.context.annotation.DeferredImportSelector.Group; import org.springframework.core.NestedIOException; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; @@ -100,6 +101,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @author Phillip Webb * @author Sam Brannen + * @author Stephane Nicoll * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -543,23 +545,48 @@ class ConfigurationClassParser { } deferredImports.sort(DEFERRED_IMPORT_COMPARATOR); + Map groupings = new LinkedHashMap<>(); + Map configurationClasses = new HashMap<>(); for (DeferredImportSelectorHolder deferredImport : deferredImports) { - ConfigurationClass configClass = deferredImport.getConfigurationClass(); - try { - String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata()); - processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false); - } - catch (BeanDefinitionStoreException ex) { - throw ex; - } - catch (Throwable ex) { - throw new BeanDefinitionStoreException( - "Failed to process import candidates for configuration class [" + - configClass.getMetadata().getClassName() + "]", ex); - } + Class group = deferredImport.getImportSelector().getImportGroup(); + DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent( + (group == null ? deferredImport : group), + (key) -> new DeferredImportSelectorGrouping(createGroup(group))); + grouping.add(deferredImport); + configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(), + deferredImport.getConfigurationClass()); + } + for (DeferredImportSelectorGrouping grouping : groupings.values()) { + grouping.getImports().forEach((entry) -> { + ConfigurationClass configurationClass = configurationClasses.get( + entry.getMetadata()); + try { + processImports(configurationClass, asSourceClass(configurationClass), + asSourceClasses(entry.getImportClassName()), false); + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to process import candidates for configuration class [" + + configurationClass.getMetadata().getClassName() + "]", ex); + } + }); } } + private Group createGroup(@Nullable Class type) { + Class effectiveType = (type != null ? type + : DefaultDeferredImportSelectorGroup.class); + Group group = BeanUtils.instantiateClass(effectiveType); + ParserStrategyUtils.invokeAwareMethods(group, + ConfigurationClassParser.this.environment, + ConfigurationClassParser.this.resourceLoader, + ConfigurationClassParser.this.registry); + return group; + } + private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection importCandidates, boolean checkForCircularImports) throws IOException { @@ -677,7 +704,7 @@ class ConfigurationClassParser { /** * Factory method to obtain {@link SourceClass}s from class names. */ - private Collection asSourceClasses(String[] classNames) throws IOException { + private Collection asSourceClasses(String... classNames) throws IOException { List annotatedClasses = new ArrayList<>(classNames.length); for (String className : classNames) { annotatedClasses.add(asSourceClass(className)); @@ -777,6 +804,52 @@ class ConfigurationClassParser { } + private static class DeferredImportSelectorGrouping { + + private final DeferredImportSelector.Group group; + + private final List deferredImports = new ArrayList<>(); + + DeferredImportSelectorGrouping(Group group) { + this.group = group; + } + + public void add(DeferredImportSelectorHolder deferredImport) { + this.deferredImports.add(deferredImport); + } + + /** + * Return the imports defined by the group. + * @return each import with its associated configuration class + */ + public Iterable getImports() { + for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { + this.group.process(deferredImport.getConfigurationClass().getMetadata(), + deferredImport.getImportSelector()); + } + return this.group.selectImports(); + } + } + + + private static class DefaultDeferredImportSelectorGroup implements Group { + + private final List imports = new ArrayList<>(); + + @Override + public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { + for (String importClassName : selector.selectImports(metadata)) { + this.imports.add(new Entry(metadata, importClassName)); + } + } + + @Override + public Iterable selectImports() { + return this.imports; + } + } + + /** * Simple wrapper that allows annotated source classes to be dealt with * in a uniform manner, regardless of how they are loaded. 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 index ceb7f9b6e32..a7dbf15b1dd 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,11 @@ package org.springframework.context.annotation; +import java.util.Objects; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; + /** * 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 @@ -25,9 +30,90 @@ package org.springframework.context.annotation; * interface or use the {@link org.springframework.core.annotation.Order} annotation to * indicate a precedence against other {@link DeferredImportSelector}s. * + *

Implementations may also provide an {@link #getImportGroup() import group} which + * can provide additional sorting and filtering logic across different selectors. + * * @author Phillip Webb + * @author Stephane Nicoll * @since 4.0 */ public interface DeferredImportSelector extends ImportSelector { + /** + * Return a specific import group or {@code null} if no grouping is required. + * @return the import group class or {@code null} + */ + @Nullable + default Class getImportGroup() { + return null; + } + + + /** + * Interface used to group results from different import selectors. + */ + interface Group { + + /** + * Process the {@link AnnotationMetadata} of the importing @{@link Configuration} + * class using the specified {@link DeferredImportSelector}. + */ + void process(AnnotationMetadata metadata, DeferredImportSelector selector); + + /** + * Return the {@link Entry entries} of which class(es) should be imported for this + * group. + */ + Iterable selectImports(); + + /** + * An entry that holds the {@link AnnotationMetadata} of the importing + * {@link Configuration} class and the class name to import. + */ + class Entry { + + private final AnnotationMetadata metadata; + + private final String importClassName; + + public Entry(AnnotationMetadata metadata, String importClassName) { + this.metadata = metadata; + this.importClassName = importClassName; + } + + /** + * Return the {@link AnnotationMetadata} of the importing + * {@link Configuration} class. + */ + public AnnotationMetadata getMetadata() { + return this.metadata; + } + + /** + * Return the fully qualified name of the class to import. + */ + public String getImportClassName() { + return this.importClassName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Entry entry = (Entry) o; + return Objects.equals(this.metadata, entry.metadata) && + Objects.equals(this.importClassName, entry.importClassName); + } + + @Override + public int hashCode() { + return Objects.hash(this.metadata, this.importClassName); + } + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java new file mode 100644 index 00000000000..5bd7fe700e4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import org.junit.Test; + +import org.springframework.context.annotation.DeferredImportSelector.Group; +import org.springframework.core.type.AnnotationMetadata; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link DeferredImportSelector}. + * + * @author Stephane Nicoll + */ +public class DeferredImportSelectorTests { + + @Test + public void entryEqualsSameInstance() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + Group.Entry entry = new Group.Entry(metadata, "com.example.Test"); + assertEquals(entry, entry); + } + + @Test + public void entryEqualsSameMetadataAndClassName() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + assertEquals(new Group.Entry(metadata, "com.example.Test"), + new Group.Entry(metadata, "com.example.Test")); + } + + @Test + public void entryEqualDifferentMetadataAndSameClassName() { + assertNotEquals( + new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test"), + new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test")); + } + + @Test + public void entryEqualSameMetadataAnDifferentClassName() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + assertNotEquals(new Group.Entry(metadata, "com.example.Test"), + new Group.Entry(metadata, "com.example.AnotherTest")); + } +} 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 index 150ba9faf6b..37a9fa05166 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -20,13 +20,19 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.hamcrest.Matcher; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -39,15 +45,20 @@ import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.*; /** * Tests for {@link ImportSelector} and {@link DeferredImportSelector}. * * @author Phillip Webb + * @author Stephane Nicoll */ @SuppressWarnings("resource") public class ImportSelectorTests { @@ -55,9 +66,11 @@ public class ImportSelectorTests { static Map, String> importFrom = new HashMap<>(); - @BeforeClass - public static void clearImportFrom() { + @Before + public void cleanup() { ImportSelectorTests.importFrom.clear(); + SampleImportSelector.cleanup(); + TestImportGroup.cleanup(); } @@ -94,6 +107,48 @@ public class ImportSelectorTests { assertThat(importFrom.get(DeferredImportSelector2.class), isFromIndirect); } + @Test + public void importSelectorsWithGroup() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(GroupedConfig.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + assertThat(TestImportGroup.instancesCount.get(), equalTo(1)); + assertThat(TestImportGroup.imports.size(), equalTo(1)); + assertThat(TestImportGroup.imports.values().iterator().next().size(), equalTo(2)); + } + + @Test + public void importSelectorsSeparateWithGroup() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(GroupedConfig1.class); + context.register(GroupedConfig2.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + assertThat(TestImportGroup.instancesCount.get(), equalTo(1)); + assertThat(TestImportGroup.imports.size(), equalTo(2)); + Iterator iterator = TestImportGroup.imports.keySet().iterator(); + assertThat(iterator.next().getClassName(), equalTo(GroupedConfig2.class.getName())); + assertThat(iterator.next().getClassName(), equalTo(GroupedConfig1.class.getName())); + } + + @Test + public void invokeAwareMethodsInImportGroup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(GroupedConfig1.class); + assertThat(TestImportGroup.beanFactory, is(context.getBeanFactory())); + assertThat(TestImportGroup.classLoader, is(context.getBeanFactory().getBeanClassLoader())); + assertThat(TestImportGroup.resourceLoader, is(notNullValue())); + assertThat(TestImportGroup.environment, is(context.getEnvironment())); + } + @Configuration @Import(SampleImportSelector.class) @@ -109,6 +164,13 @@ public class ImportSelectorTests { static BeanFactory beanFactory; static Environment environment; + static void cleanup() { + SampleImportSelector.classLoader = null; + SampleImportSelector.beanFactory = null; + SampleImportSelector.resourceLoader = null; + SampleImportSelector.environment = null; + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { SampleImportSelector.classLoader = classLoader; @@ -255,4 +317,107 @@ public class ImportSelectorTests { public static class IndirectImport { } + + @GroupedSample + @Configuration + static class GroupedConfig { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import({GroupedDeferredImportSelector1.class, GroupedDeferredImportSelector2.class, ImportSelector1.class, ImportSelector2.class}) + public @interface GroupedSample { + } + + @Configuration + @Import(GroupedDeferredImportSelector1.class) + static class GroupedConfig1 { + } + + @Configuration + @Import(GroupedDeferredImportSelector2.class) + static class GroupedConfig2 { + } + + + public static class GroupedDeferredImportSelector1 extends DeferredImportSelector1 { + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + } + + public static class GroupedDeferredImportSelector2 extends DeferredImportSelector2 { + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + } + + + public static class TestImportGroup implements DeferredImportSelector.Group, + BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { + + static ClassLoader classLoader; + static ResourceLoader resourceLoader; + static BeanFactory beanFactory; + static Environment environment; + + static AtomicInteger instancesCount = new AtomicInteger(); + static MultiValueMap imports = new LinkedMultiValueMap<>(); + + public TestImportGroup() { + TestImportGroup.instancesCount.incrementAndGet(); + } + + static void cleanup() { + TestImportGroup.classLoader = null; + TestImportGroup.beanFactory = null; + TestImportGroup.resourceLoader = null; + TestImportGroup.environment = null; + TestImportGroup.instancesCount = new AtomicInteger(); + TestImportGroup.imports.clear(); + } + + @Override + public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { + TestImportGroup.imports.addAll(metadata, + Arrays.asList(selector.selectImports(metadata))); + } + + @Override + public Iterable selectImports() { + LinkedList content = new LinkedList<>(); + TestImportGroup.imports.forEach((metadata, values) -> + values.forEach(value -> content.add(new Entry(metadata, value)))); + Collections.reverse(content); + return content; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + TestImportGroup.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + TestImportGroup.beanFactory = beanFactory; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + TestImportGroup.resourceLoader = resourceLoader; + } + + @Override + public void setEnvironment(Environment environment) { + TestImportGroup.environment = environment; + } + } + }