Commit bc7db86c authored by Stephane Nicoll's avatar Stephane Nicoll

Consistently apply exclude on auto-configuration

Previously, exclude of an import selector was applied only locally. In
other words, if one import selector imports `AcmeAutoConfiguration` and
another one exclude it, it would still be imported because exclude were
applied separately

This commit collects the outcome of all auto-configuration import
selectors and then apply exclusions in a single pass.

Closes gh-12586
parent 184cd0c7
......@@ -18,7 +18,9 @@ package org.springframework.boot.autoconfigure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
......@@ -40,6 +42,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
......@@ -70,6 +73,8 @@ public class AutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
private static final String[] NO_IMPORTS = {};
private static final Log logger = LogFactory
......@@ -92,6 +97,24 @@ public class AutoConfigurationImportSelector
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing @{@link Configuration} class.
* @param autoConfigurationMetadata the auto-configuration metadata
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
......@@ -101,7 +124,7 @@ public class AutoConfigurationImportSelector
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return StringUtils.toStringArray(configurations);
return new AutoConfigurationEntry(configurations, exclusions);
}
@Override
......@@ -357,13 +380,17 @@ public class AutoConfigurationImportSelector
private static class AutoConfigurationGroup implements DeferredImportSelector.Group,
BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>();
private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>();
private ClassLoader beanClassLoader;
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>();
private AutoConfigurationMetadata autoConfigurationMetadata;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
......@@ -383,29 +410,63 @@ public class AutoConfigurationImportSelector
@Override
public void process(AnnotationMetadata annotationMetadata,
DeferredImportSelector deferredImportSelector) {
String[] imports = deferredImportSelector.selectImports(annotationMetadata);
for (String importClassName : imports) {
this.entries.put(importClassName, annotationMetadata);
Assert.state(
deferredImportSelector instanceof AutoConfigurationImportSelector,
String.format(
"AutoConfigurationImportSelector only supports %s implementations, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(),
annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
@Override
public Iterable<Entry> selectImports() {
return sortAutoConfigurations().stream()
.map((importClassName) -> new Entry(this.entries.get(importClassName),
importClassName))
.collect(Collectors.toList());
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
}
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions)
.flatMap(Collection::stream).collect(Collectors.toSet());
Set<String> processedConfigurations = new LinkedHashSet<>();
Set<String> processedExclusions = new LinkedHashSet<>();
this.autoConfigurationEntries.forEach((entry) -> {
List<String> configurations = new ArrayList<>(entry.getConfigurations());
configurations.removeAll(allExclusions);
configurations.removeIf(processedConfigurations::contains);
Set<String> exclusions = new HashSet<>(entry.getExclusions());
exclusions.removeIf(processedExclusions::contains);
// This now represents the exact state of this entry based on the
// state of all other entries
processedConfigurations.addAll(configurations);
processedExclusions.addAll(exclusions);
});
return sortAutoConfigurations(processedConfigurations,
getAutoConfigurationMetadata())
.stream()
.map((importClassName) -> new Entry(
this.entries.get(importClassName), importClassName))
.collect(Collectors.toList());
}
private List<String> sortAutoConfigurations() {
List<String> autoConfigurations = new ArrayList<>(this.entries.keySet());
if (this.entries.size() <= 1) {
return autoConfigurations;
private AutoConfigurationMetadata getAutoConfigurationMetadata() {
if (this.autoConfigurationMetadata == null) {
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
return this.autoConfigurationMetadata;
}
private List<String> sortAutoConfigurations(Set<String> configurations,
AutoConfigurationMetadata autoConfigurationMetadata) {
return new AutoConfigurationSorter(getMetadataReaderFactory(),
autoConfigurationMetadata).getInPriorityOrder(autoConfigurations);
autoConfigurationMetadata).getInPriorityOrder(configurations);
}
private MetadataReaderFactory getMetadataReaderFactory() {
......@@ -421,4 +482,37 @@ public class AutoConfigurationImportSelector
}
protected static class AutoConfigurationEntry {
private final List<String> configurations;
private final Set<String> exclusions;
private AutoConfigurationEntry() {
this.configurations = Collections.EMPTY_LIST;
this.exclusions = Collections.EMPTY_SET;
}
/**
* Create an entry with the configurations that were contributed and their
* exclusions.
* @param configurations the configurations that should be imported
* @param exclusions the exclusions that were applied to the original list
*/
AutoConfigurationEntry(Collection<String> configurations,
Collection<String> exclusions) {
this.configurations = new ArrayList<>(configurations);
this.exclusions = new HashSet<>(exclusions);
}
public List<String> getConfigurations() {
return this.configurations;
}
public Set<String> getExclusions() {
return this.exclusions;
}
}
}
......@@ -43,6 +43,7 @@ import org.springframework.util.ObjectUtils;
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public final class ConditionEvaluationReport {
......@@ -56,9 +57,9 @@ public final class ConditionEvaluationReport {
private ConditionEvaluationReport parent;
private List<String> exclusions = Collections.emptyList();
private final List<String> exclusions = new ArrayList<>();
private Set<String> unconditionalClasses = new HashSet<>();
private final Set<String> unconditionalClasses = new HashSet<>();
/**
* Private constructor.
......@@ -92,7 +93,7 @@ public final class ConditionEvaluationReport {
*/
public void recordExclusions(Collection<String> exclusions) {
Assert.notNull(exclusions, "exclusions must not be null");
this.exclusions = new ArrayList<>(exclusions);
this.exclusions.addAll(exclusions);
}
/**
......@@ -102,7 +103,7 @@ public final class ConditionEvaluationReport {
*/
public void recordEvaluationCandidates(List<String> evaluationCandidates) {
Assert.notNull(evaluationCandidates, "evaluationCandidates must not be null");
this.unconditionalClasses = new HashSet<>(evaluationCandidates);
this.unconditionalClasses.addAll(evaluationCandidates);
}
/**
......@@ -145,7 +146,9 @@ public final class ConditionEvaluationReport {
* @return the names of the unconditional classes
*/
public Set<String> getUnconditionalClasses() {
return Collections.unmodifiableSet(this.unconditionalClasses);
Set<String> filtered = new HashSet<>(this.unconditionalClasses);
filtered.removeIf(this.exclusions::contains);
return Collections.unmodifiableSet(filtered);
}
/**
......
......@@ -56,7 +56,19 @@ public class ImportAutoConfigurationTests {
.containsExactly("ConfigA", "ConfigB", "ConfigD");
}
private List<String> getImportedConfigBeans(Class<?> config) {
@Test
public void excludeAppliedGlobally() {
assertThat(getImportedConfigBeans(ExcludeDConfig.class, ImportADConfig.class))
.containsExactly("ConfigA");
}
@Test
public void excludeWithRedundancy() {
assertThat(getImportedConfigBeans(ExcludeADConfig.class, ExcludeDConfig.class,
ImportADConfig.class)).isEmpty();
}
private List<String> getImportedConfigBeans(Class<?>... config) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
config);
String shortName = ClassUtils.getShortName(ImportAutoConfigurationTests.class);
......@@ -97,6 +109,21 @@ public class ImportAutoConfigurationTests {
}
@ImportAutoConfiguration(classes = { ConfigA.class, ConfigD.class })
static class ImportADConfig {
}
@ImportAutoConfiguration(exclude = { ConfigA.class, ConfigD.class })
static class ExcludeADConfig {
}
@ImportAutoConfiguration(exclude = ConfigD.class)
static class ExcludeDConfig {
}
@Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration({ ConfigC.class, ConfigA.class })
@interface MetaImportAutoConfiguration {
......
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-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,7 @@
package org.springframework.boot.autoconfigure.condition;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
......@@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link ConditionEvaluationReportAutoConfigurationImportListener}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ConditionEvaluationReportAutoConfigurationImportListenerTests {
......@@ -81,4 +83,32 @@ public class ConditionEvaluationReportAutoConfigurationImportListenerTests {
assertThat(report.getExclusions()).containsExactlyElementsOf(exclusions);
}
@Test
public void onAutoConfigurationImportEventShouldApplyExclusionsGlobally() {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
Arrays.asList("First", "Second"), Collections.emptySet());
this.listener.onAutoConfigurationImportEvent(event);
AutoConfigurationImportEvent anotherEvent = new AutoConfigurationImportEvent(this,
Collections.emptyList(), Collections.singleton("First"));
this.listener.onAutoConfigurationImportEvent(anotherEvent);
ConditionEvaluationReport report = ConditionEvaluationReport
.get(this.beanFactory);
assertThat(report.getUnconditionalClasses()).containsExactly("Second");
assertThat(report.getExclusions()).containsExactly("First");
}
@Test
public void onAutoConfigurationImportEventShouldApplyExclusionsGloballyWhenExclusionIsAlreadyApplied() {
AutoConfigurationImportEvent excludeEvent = new AutoConfigurationImportEvent(this,
Collections.emptyList(), Collections.singleton("First"));
this.listener.onAutoConfigurationImportEvent(excludeEvent);
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
Arrays.asList("First", "Second"), Collections.emptySet());
this.listener.onAutoConfigurationImportEvent(event);
ConditionEvaluationReport report = ConditionEvaluationReport
.get(this.beanFactory);
assertThat(report.getUnconditionalClasses()).containsExactly("Second");
assertThat(report.getExclusions()).containsExactly("First");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment