mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-09-03 04:26:12 +08:00
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:
parent
75186d42da
commit
54aeff47a2
@ -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 {};
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user