diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfiguration.java index 14adcc8aed..ce214673c2 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfiguration.java @@ -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 {}; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java index 2700958b3f..90abf076f8 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java @@ -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 getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { - try { - return getCandidateConfigurations( - ClassUtils.forName(metadata.getClassName(), null)); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - private List getCandidateConfigurations(Class source) { - Set candidates = new LinkedHashSet(); - collectCandidateConfigurations(source, candidates, new HashSet>()); - return new ArrayList(candidates); - } - - private void collectCandidateConfigurations(Class source, Set candidates, - Set> 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); + List candidates = new ArrayList(); + Map, List> annotations = getAnnotations(metadata); + for (Map.Entry, List> entry : annotations.entrySet()) { + collectCandidateConfigurations(entry.getKey(), entry.getValue(), candidates); } + return candidates; } - private void collectCandidateConfigurations(Class source, Annotation annotation, - Set candidates, Set> seen) { - if (ANNOTATION_NAMES.contains(annotation.annotationType().getName())) { + private void collectCandidateConfigurations(Class source, + List annotations, List candidates) { + for (Annotation annotation : annotations) { candidates.addAll(getConfigurationsForAnnotation(source, annotation)); } - collectCandidateConfigurations(annotation.annotationType(), candidates, seen); } private Collection 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 getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) { - return Collections.emptySet(); + Set exclusions = new LinkedHashSet(); + 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 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, List> getAnnotations(AnnotationMetadata metadata) { + MultiValueMap, Annotation> annotations = new LinkedMultiValueMap, Annotation>(); + Class source = ClassUtils.resolveClassName(metadata.getClassName(), null); + collectAnnotations(source, annotations, new HashSet>()); + return Collections.unmodifiableMap(annotations); + } + + private void collectAnnotations(Class source, + MultiValueMap, Annotation> annotations, HashSet> 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 diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java index dca862fcca..60f274ecbb 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java @@ -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 {}; + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationTests.java index 8cd11c1a36..047bdacfc5 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationTests.java @@ -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 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 { diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTest.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTest.java index 232bb31b44..6c3cbd34ba 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTest.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTest.java @@ -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 {}; + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTest.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTest.java index abc8ebf59e..676459c585 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTest.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTest.java @@ -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 {}; + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java index af59751113..b8e6940100 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java @@ -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 {}; + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java index bc66880ebc..5765964a50 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java @@ -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 {}; + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java index 33902a72a3..8ad305b6eb 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java @@ -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 {}; + }