Skip to content

Commit cc12afd

Browse files
committed
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
1 parent 5f4d5f1 commit cc12afd

File tree

4 files changed

+403
-18
lines changed

4 files changed

+403
-18
lines changed

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

+87-14
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
5252
import org.springframework.beans.factory.support.BeanNameGenerator;
5353
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
54+
import org.springframework.context.annotation.DeferredImportSelector.Group;
5455
import org.springframework.core.NestedIOException;
5556
import org.springframework.core.OrderComparator;
5657
import org.springframework.core.Ordered;
@@ -100,6 +101,7 @@
100101
* @author Juergen Hoeller
101102
* @author Phillip Webb
102103
* @author Sam Brannen
104+
* @author Stephane Nicoll
103105
* @since 3.0
104106
* @see ConfigurationClassBeanDefinitionReader
105107
*/
@@ -543,23 +545,48 @@ private void processDeferredImportSelectors() {
543545
}
544546

545547
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
548+
Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
549+
Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
546550
for (DeferredImportSelectorHolder deferredImport : deferredImports) {
547-
ConfigurationClass configClass = deferredImport.getConfigurationClass();
548-
try {
549-
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
550-
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
551-
}
552-
catch (BeanDefinitionStoreException ex) {
553-
throw ex;
554-
}
555-
catch (Throwable ex) {
556-
throw new BeanDefinitionStoreException(
557-
"Failed to process import candidates for configuration class [" +
558-
configClass.getMetadata().getClassName() + "]", ex);
559-
}
551+
Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
552+
DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent(
553+
(group == null ? deferredImport : group),
554+
(key) -> new DeferredImportSelectorGrouping(createGroup(group)));
555+
grouping.add(deferredImport);
556+
configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
557+
deferredImport.getConfigurationClass());
558+
}
559+
for (DeferredImportSelectorGrouping grouping : groupings.values()) {
560+
grouping.getImports().forEach((entry) -> {
561+
ConfigurationClass configurationClass = configurationClasses.get(
562+
entry.getMetadata());
563+
try {
564+
processImports(configurationClass, asSourceClass(configurationClass),
565+
asSourceClasses(entry.getImportClassName()), false);
566+
}
567+
catch (BeanDefinitionStoreException ex) {
568+
throw ex;
569+
}
570+
catch (Throwable ex) {
571+
throw new BeanDefinitionStoreException(
572+
"Failed to process import candidates for configuration class [" +
573+
configurationClass.getMetadata().getClassName() + "]", ex);
574+
}
575+
});
560576
}
561577
}
562578

579+
private Group createGroup(@Nullable Class<? extends Group> type) {
580+
Class<? extends Group> effectiveType = (type != null ? type
581+
: DefaultDeferredImportSelectorGroup.class);
582+
Group group = BeanUtils.instantiateClass(effectiveType);
583+
ParserStrategyUtils.invokeAwareMethods(group,
584+
ConfigurationClassParser.this.environment,
585+
ConfigurationClassParser.this.resourceLoader,
586+
ConfigurationClassParser.this.registry);
587+
return group;
588+
}
589+
563590
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
564591
Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {
565592

@@ -677,7 +704,7 @@ SourceClass asSourceClass(@Nullable Class<?> classType) throws IOException {
677704
/**
678705
* Factory method to obtain {@link SourceClass}s from class names.
679706
*/
680-
private Collection<SourceClass> asSourceClasses(String[] classNames) throws IOException {
707+
private Collection<SourceClass> asSourceClasses(String... classNames) throws IOException {
681708
List<SourceClass> annotatedClasses = new ArrayList<>(classNames.length);
682709
for (String className : classNames) {
683710
annotatedClasses.add(asSourceClass(className));
@@ -777,6 +804,52 @@ public DeferredImportSelector getImportSelector() {
777804
}
778805

779806

807+
private static class DeferredImportSelectorGrouping {
808+
809+
private final DeferredImportSelector.Group group;
810+
811+
private final List<DeferredImportSelectorHolder> deferredImports = new ArrayList<>();
812+
813+
DeferredImportSelectorGrouping(Group group) {
814+
this.group = group;
815+
}
816+
817+
public void add(DeferredImportSelectorHolder deferredImport) {
818+
this.deferredImports.add(deferredImport);
819+
}
820+
821+
/**
822+
* Return the imports defined by the group.
823+
* @return each import with its associated configuration class
824+
*/
825+
public Iterable<Group.Entry> getImports() {
826+
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
827+
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
828+
deferredImport.getImportSelector());
829+
}
830+
return this.group.selectImports();
831+
}
832+
}
833+
834+
835+
private static class DefaultDeferredImportSelectorGroup implements Group {
836+
837+
private final List<Entry> imports = new ArrayList<>();
838+
839+
@Override
840+
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
841+
for (String importClassName : selector.selectImports(metadata)) {
842+
this.imports.add(new Entry(metadata, importClassName));
843+
}
844+
}
845+
846+
@Override
847+
public Iterable<Entry> selectImports() {
848+
return this.imports;
849+
}
850+
}
851+
852+
780853
/**
781854
* Simple wrapper that allows annotated source classes to be dealt with
782855
* in a uniform manner, regardless of how they are loaded.

spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,11 @@
1616

1717
package org.springframework.context.annotation;
1818

19+
import java.util.Objects;
20+
21+
import org.springframework.core.type.AnnotationMetadata;
22+
import org.springframework.lang.Nullable;
23+
1924
/**
2025
* A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans
2126
* have been processed. This type of selector can be particularly useful when the selected
@@ -25,9 +30,90 @@
2530
* interface or use the {@link org.springframework.core.annotation.Order} annotation to
2631
* indicate a precedence against other {@link DeferredImportSelector}s.
2732
*
33+
* <p>Implementations may also provide an {@link #getImportGroup() import group} which
34+
* can provide additional sorting and filtering logic across different selectors.
35+
*
2836
* @author Phillip Webb
37+
* @author Stephane Nicoll
2938
* @since 4.0
3039
*/
3140
public interface DeferredImportSelector extends ImportSelector {
3241

42+
/**
43+
* Return a specific import group or {@code null} if no grouping is required.
44+
* @return the import group class or {@code null}
45+
*/
46+
@Nullable
47+
default Class<? extends Group> getImportGroup() {
48+
return null;
49+
}
50+
51+
52+
/**
53+
* Interface used to group results from different import selectors.
54+
*/
55+
interface Group {
56+
57+
/**
58+
* Process the {@link AnnotationMetadata} of the importing @{@link Configuration}
59+
* class using the specified {@link DeferredImportSelector}.
60+
*/
61+
void process(AnnotationMetadata metadata, DeferredImportSelector selector);
62+
63+
/**
64+
* Return the {@link Entry entries} of which class(es) should be imported for this
65+
* group.
66+
*/
67+
Iterable<Entry> selectImports();
68+
69+
/**
70+
* An entry that holds the {@link AnnotationMetadata} of the importing
71+
* {@link Configuration} class and the class name to import.
72+
*/
73+
class Entry {
74+
75+
private final AnnotationMetadata metadata;
76+
77+
private final String importClassName;
78+
79+
public Entry(AnnotationMetadata metadata, String importClassName) {
80+
this.metadata = metadata;
81+
this.importClassName = importClassName;
82+
}
83+
84+
/**
85+
* Return the {@link AnnotationMetadata} of the importing
86+
* {@link Configuration} class.
87+
*/
88+
public AnnotationMetadata getMetadata() {
89+
return this.metadata;
90+
}
91+
92+
/**
93+
* Return the fully qualified name of the class to import.
94+
*/
95+
public String getImportClassName() {
96+
return this.importClassName;
97+
}
98+
99+
@Override
100+
public boolean equals(Object o) {
101+
if (this == o) {
102+
return true;
103+
}
104+
if (o == null || getClass() != o.getClass()) {
105+
return false;
106+
}
107+
Entry entry = (Entry) o;
108+
return Objects.equals(this.metadata, entry.metadata) &&
109+
Objects.equals(this.importClassName, entry.importClassName);
110+
}
111+
112+
@Override
113+
public int hashCode() {
114+
return Objects.hash(this.metadata, this.importClassName);
115+
}
116+
}
117+
}
118+
33119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.annotation;
18+
19+
import org.junit.Test;
20+
21+
import org.springframework.context.annotation.DeferredImportSelector.Group;
22+
import org.springframework.core.type.AnnotationMetadata;
23+
24+
import static org.junit.Assert.*;
25+
import static org.mockito.Mockito.*;
26+
27+
/**
28+
* Tests for {@link DeferredImportSelector}.
29+
*
30+
* @author Stephane Nicoll
31+
*/
32+
public class DeferredImportSelectorTests {
33+
34+
@Test
35+
public void entryEqualsSameInstance() {
36+
AnnotationMetadata metadata = mock(AnnotationMetadata.class);
37+
Group.Entry entry = new Group.Entry(metadata, "com.example.Test");
38+
assertEquals(entry, entry);
39+
}
40+
41+
@Test
42+
public void entryEqualsSameMetadataAndClassName() {
43+
AnnotationMetadata metadata = mock(AnnotationMetadata.class);
44+
assertEquals(new Group.Entry(metadata, "com.example.Test"),
45+
new Group.Entry(metadata, "com.example.Test"));
46+
}
47+
48+
@Test
49+
public void entryEqualDifferentMetadataAndSameClassName() {
50+
assertNotEquals(
51+
new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test"),
52+
new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test"));
53+
}
54+
55+
@Test
56+
public void entryEqualSameMetadataAnDifferentClassName() {
57+
AnnotationMetadata metadata = mock(AnnotationMetadata.class);
58+
assertNotEquals(new Group.Entry(metadata, "com.example.Test"),
59+
new Group.Entry(metadata, "com.example.AnotherTest"));
60+
}
61+
}

0 commit comments

Comments
 (0)