Add exclude support to @ImportAutoConfiguration

Update `@ImportAutoConfiguration` with support for an exclude attribute
that works in a similar way to `@EnableAutoConfiguration`.

Also update existing `@Test...` annotation with exclude attribute
aliases.

Fixes gh-6809
This commit is contained in:
Phillip Webb 2016-12-29 22:43:44 -08:00
parent 75186d42da
commit 54aeff47a2
9 changed files with 198 additions and 39 deletions

View File

@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* Import and apply the specified auto-configuration classes. Applies the same ordering
@ -46,12 +47,27 @@ import org.springframework.context.annotation.Import;
@Import(ImportAutoConfigurationImportSelector.class)
public @interface ImportAutoConfiguration {
/**
* The auto-configuration classes that should be imported. This is an alias for
* {@link #classes()}.
* @return the classes to import
*/
@AliasFor("classes")
Class<?>[] value() default {};
/**
* The auto-configuration classes that should be imported. When empty, the classes are
* specified using an entry in {@code META-INF/spring.factories} where the key is the
* fully-qualified name of the annotated class.
* @return the classes to import
*/
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
}

View File

@ -24,13 +24,18 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* Variant of {@link EnableAutoConfigurationImportSelector} for
@ -59,47 +64,27 @@ class ImportAutoConfigurationImportSelector
@Override
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
try {
return getCandidateConfigurations(
ClassUtils.forName(metadata.getClassName(), null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
List<String> candidates = new ArrayList<String>();
Map<Class<?>, List<Annotation>> annotations = getAnnotations(metadata);
for (Map.Entry<Class<?>, List<Annotation>> entry : annotations.entrySet()) {
collectCandidateConfigurations(entry.getKey(), entry.getValue(), candidates);
}
return candidates;
}
private List<String> getCandidateConfigurations(Class<?> source) {
Set<String> candidates = new LinkedHashSet<String>();
collectCandidateConfigurations(source, candidates, new HashSet<Class<?>>());
return new ArrayList<String>(candidates);
}
private void collectCandidateConfigurations(Class<?> source, Set<String> candidates,
Set<Class<?>> seen) {
if (source != null && seen.add(source)) {
for (Annotation annotation : source.getDeclaredAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
collectCandidateConfigurations(source, annotation, candidates, seen);
}
}
collectCandidateConfigurations(source.getSuperclass(), candidates, seen);
}
}
private void collectCandidateConfigurations(Class<?> source, Annotation annotation,
Set<String> candidates, Set<Class<?>> seen) {
if (ANNOTATION_NAMES.contains(annotation.annotationType().getName())) {
private void collectCandidateConfigurations(Class<?> source,
List<Annotation> annotations, List<String> candidates) {
for (Annotation annotation : annotations) {
candidates.addAll(getConfigurationsForAnnotation(source, annotation));
}
collectCandidateConfigurations(annotation.annotationType(), candidates, seen);
}
private Collection<String> getConfigurationsForAnnotation(Class<?> source,
Annotation annotation) {
String[] value = (String[]) AnnotationUtils
.getAnnotationAttributes(annotation, true).get("value");
if (value.length > 0) {
return Arrays.asList(value);
String[] classes = (String[]) AnnotationUtils
.getAnnotationAttributes(annotation, true).get("classes");
if (classes.length > 0) {
return Arrays.asList(classes);
}
return SpringFactoriesLoader.loadFactoryNames(source,
getClass().getClassLoader());
@ -108,7 +93,52 @@ class ImportAutoConfigurationImportSelector
@Override
protected Set<String> getExclusions(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
return Collections.emptySet();
Set<String> exclusions = new LinkedHashSet<String>();
Class<?> source = ClassUtils.resolveClassName(metadata.getClassName(), null);
for (String annotationName : ANNOTATION_NAMES) {
AnnotationAttributes merged = AnnotatedElementUtils
.getMergedAnnotationAttributes(source, annotationName);
Class<?>[] exclude = (merged == null ? null
: merged.getClassArray("exclude"));
if (exclude != null) {
for (Class<?> excludeClass : exclude) {
exclusions.add(excludeClass.getName());
}
}
}
for (List<Annotation> annotations : getAnnotations(metadata).values()) {
for (Annotation annotation : annotations) {
String[] exclude = (String[]) AnnotationUtils
.getAnnotationAttributes(annotation, true).get("exclude");
if (!ObjectUtils.isEmpty(exclude)) {
exclusions.addAll(Arrays.asList(exclude));
}
}
}
return exclusions;
}
private Map<Class<?>, List<Annotation>> getAnnotations(AnnotationMetadata metadata) {
MultiValueMap<Class<?>, Annotation> annotations = new LinkedMultiValueMap<Class<?>, Annotation>();
Class<?> source = ClassUtils.resolveClassName(metadata.getClassName(), null);
collectAnnotations(source, annotations, new HashSet<Class<?>>());
return Collections.unmodifiableMap(annotations);
}
private void collectAnnotations(Class<?> source,
MultiValueMap<Class<?>, Annotation> annotations, HashSet<Class<?>> seen) {
if (source != null && seen.add(source)) {
for (Annotation annotation : source.getDeclaredAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
if (ANNOTATION_NAMES
.contains(annotation.annotationType().getName())) {
annotations.add(source, annotation);
}
collectAnnotations(annotation.annotationType(), annotations, seen);
}
}
collectAnnotations(source.getSuperclass(), annotations, seen);
}
}
@Override

View File

@ -29,6 +29,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
@ -69,6 +70,15 @@ public class ImportAutoConfigurationImportSelectorTests {
assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName());
}
@Test
public void importsAreSelectedUsingClassesAttribute() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory()
.getMetadataReader(ImportFreeMarkerUsingClassesAttribute.class.getName())
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName());
}
@Test
public void propertyExclusionsAreNotApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory()
@ -97,22 +107,58 @@ public class ImportAutoConfigurationImportSelectorTests {
assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName());
}
@Test
public void exclusionsAreApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory()
.getMetadataReader(MultipleImportsWithExclusion.class.getName())
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName());
}
@Test
public void exclusionsAliasesAreApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory()
.getMetadataReader(
ImportWithSelfAnnotatingAnnotationExclude.class.getName())
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).isEmpty();
}
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
static class ImportFreeMarker {
}
@ImportAutoConfiguration(classes = FreeMarkerAutoConfiguration.class)
static class ImportFreeMarkerUsingClassesAttribute {
}
@ImportOne
@ImportTwo
static class MultipleImports {
}
@ImportOne
@ImportTwo
@ImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class)
static class MultipleImportsWithExclusion {
}
@SelfAnnotating
static class ImportWithSelfAnnotatingAnnotation {
}
@SelfAnnotating(excludeAutoConfiguration = ThymeleafAutoConfiguration.class)
static class ImportWithSelfAnnotatingAnnotationExclude {
}
@Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
static @interface ImportOne {
@ -130,6 +176,9 @@ public class ImportAutoConfigurationImportSelectorTests {
@SelfAnnotating
static @interface SelfAnnotating {
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}
}

View File

@ -38,11 +38,25 @@ public class ImportAutoConfigurationTests {
@Test
public void multipleAnnotationsShouldMergeCorrectly() {
testConfigImports(Config.class);
testConfigImports(AnotherConfig.class);
assertThat(getImportedConfigBeans(Config.class)).containsExactly("ConfigA",
"ConfigB", "ConfigC", "ConfigD");
assertThat(getImportedConfigBeans(AnotherConfig.class)).containsExactly("ConfigA",
"ConfigB", "ConfigC", "ConfigD");
}
private void testConfigImports(Class<?> config) {
@Test
public void classesAsAnAlias() throws Exception {
assertThat(getImportedConfigBeans(AnotherConfigUsingClasses.class))
.containsExactly("ConfigA", "ConfigB", "ConfigC", "ConfigD");
}
@Test
public void excluding() throws Exception {
assertThat(getImportedConfigBeans(ExcludingConfig.class))
.containsExactly("ConfigA", "ConfigB", "ConfigD");
}
private List<String> getImportedConfigBeans(Class<?> config) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
config);
String shortName = ClassUtils.getShortName(ImportAutoConfigurationTests.class);
@ -54,9 +68,8 @@ public class ImportAutoConfigurationTests {
orderedConfigBeans.add(shortBeanName.substring(beginIndex));
}
}
assertThat(orderedConfigBeans).containsExactly("ConfigA", "ConfigB", "ConfigC",
"ConfigD");
context.close();
return orderedConfigBeans;
}
@ImportAutoConfiguration({ ConfigD.class, ConfigB.class })
@ -71,6 +84,19 @@ public class ImportAutoConfigurationTests {
}
@MetaImportAutoConfiguration
@ImportAutoConfiguration(classes = { ConfigB.class, ConfigD.class })
static class AnotherConfigUsingClasses {
}
@ImportAutoConfiguration(classes = { ConfigD.class,
ConfigB.class }, exclude = ConfigC.class)
@MetaImportAutoConfiguration
static class ExcludingConfig {
}
@Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration({ ConfigC.class, ConfigA.class })
@interface MetaImportAutoConfiguration {

View File

@ -31,6 +31,7 @@ import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.BootstrapWith;
import org.springframework.transaction.annotation.Transactional;
@ -95,4 +96,11 @@ public @interface JdbcTest {
*/
ComponentScan.Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@ -32,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.boot.test.json.GsonTester;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.BootstrapWith;
/**
@ -90,4 +91,11 @@ public @interface JsonTest {
*/
Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@ -33,6 +33,7 @@ import org.springframework.boot.test.autoconfigure.properties.PropertyMapping;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.BootstrapWith;
import org.springframework.transaction.annotation.Transactional;
@ -106,4 +107,11 @@ public @interface DataJpaTest {
*/
Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@ -115,4 +115,11 @@ public @interface RestClientTest {
*/
ComponentScan.Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@ -132,4 +132,11 @@ public @interface WebMvcTest {
@AliasFor(annotation = AutoConfigureMockMvc.class, attribute = "secure")
boolean secure() default true;
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}