Report non-matching outer class conditions

Update ConditionEvaluationReport so that, whenever a negative outcome
is added for a source, any existing outcomes for inner classes of that
source are updated with a non-matching outcome that indicates that the
outer configuration did not match.

Conditions are evaluated in two phases; PARSE_CONFIGURATION first and
REGISTER_BEAN second. If a parent class’s conditions match in
PARSE_CONFIGURATION then its inner classes will have their
PARSE_CONFIGURATION conditions evaluated. If they all match, the inner
class will be reported as a positive match in the auto-configuration
report even if the outer class does not match as a result of the
subsequent evaluation of a REGISTER_BEAN condition.

Fixes gh-2122
This commit is contained in:
Andy Wilkinson 2015-01-14 18:04:58 +00:00 committed by Phillip Webb
parent 636898f9ad
commit 11b7fd832d
2 changed files with 144 additions and 5 deletions

View File

@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@ -27,6 +28,8 @@ import java.util.TreeMap;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@ -36,13 +39,18 @@ import org.springframework.util.ObjectUtils;
* @author Greg Turnquist
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ConditionEvaluationReport {
private static final String BEAN_NAME = "autoConfigurationReport";
private static final AncestorsMatchedCondition ANCESTOR_CONDITION = new AncestorsMatchedCondition();
private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>();
private boolean addedAncestorOutcomes;
private ConditionEvaluationReport parent;
/**
@ -67,6 +75,7 @@ public class ConditionEvaluationReport {
this.outcomes.put(source, new ConditionAndOutcomes());
}
this.outcomes.get(source).add(condition, outcome);
this.addedAncestorOutcomes = false;
}
/**
@ -74,9 +83,28 @@ public class ConditionEvaluationReport {
* @return the condition outcomes
*/
public Map<String, ConditionAndOutcomes> getConditionAndOutcomesBySource() {
if (!this.addedAncestorOutcomes) {
for (Map.Entry<String, ConditionAndOutcomes> entry : this.outcomes.entrySet()) {
if (!entry.getValue().isFullMatch()) {
addNoMatchOutcomeToAncestors(entry.getKey());
}
}
this.addedAncestorOutcomes = true;
}
return Collections.unmodifiableMap(this.outcomes);
}
private void addNoMatchOutcomeToAncestors(String source) {
String prefix = source + "$";
for (Entry<String, ConditionAndOutcomes> entry : this.outcomes.entrySet()) {
if (entry.getKey().startsWith(prefix)) {
ConditionOutcome outcome = new ConditionOutcome(false, "Ancestor '"
+ source + "' did not match");
entry.getValue().add(ANCESTOR_CONDITION, outcome);
}
}
}
/**
* The parent report (from a parent BeanFactory if there is one).
* @return the parent report (or null if there isn't one)
@ -186,6 +214,20 @@ public class ConditionEvaluationReport {
public int hashCode() {
return this.condition.getClass().hashCode() * 31 + this.outcome.hashCode();
}
@Override
public String toString() {
return this.condition.getClass() + " " + this.outcome;
}
}
private static class AncestorsMatchedCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@ -26,16 +26,23 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@ -51,7 +58,7 @@ import static org.junit.Assert.assertThat;
* @author Greg Turnquist
* @author Phillip Webb
*/
public class AutoConfigurationReportTests {
public class ConditionEvaluationReportTests {
private DefaultListableBeanFactory beanFactory;
@ -225,6 +232,23 @@ public class AutoConfigurationReportTests {
context.close();
}
@Test
public void negativeOuterPositiveInnerBean() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
EnvironmentTestUtils.addEnvironment(context, "test.present=true");
context.register(NegativeOuterConfig.class);
context.refresh();
ConditionEvaluationReport report = ConditionEvaluationReport.get(context
.getBeanFactory());
Map<String, ConditionAndOutcomes> sourceOutcomes = report
.getConditionAndOutcomesBySource();
assertThat(context.containsBean("negativeOuterPositiveInnerBean"), equalTo(false));
String negativeConfig = NegativeOuterConfig.class.getName();
assertThat(sourceOutcomes.get(negativeConfig).isFullMatch(), equalTo(false));
String positiveConfig = NegativeOuterConfig.PositiveInnerConfig.class.getName();
assertThat(sourceOutcomes.get(positiveConfig).isFullMatch(), equalTo(false));
}
private int getNumberOfOutcomes(ConditionAndOutcomes outcomes) {
Iterator<ConditionAndOutcome> iterator = outcomes.iterator();
int numberOfOutcomesAdded = 0;
@ -235,16 +259,89 @@ public class AutoConfigurationReportTests {
return numberOfOutcomesAdded;
}
@Configurable
@Configuration
@Import(WebMvcAutoConfiguration.class)
static class Config {
}
@Configurable
@Configuration
@Import(MultipartAutoConfiguration.class)
static class DuplicateConfig {
}
@Configuration
@Conditional({ ConditionEvaluationReportTests.MatchParseCondition.class,
ConditionEvaluationReportTests.NoMatchBeanCondition.class })
public static class NegativeOuterConfig {
@Configuration
@Conditional({ ConditionEvaluationReportTests.MatchParseCondition.class })
public static class PositiveInnerConfig {
@Bean
public String negativeOuterPositiveInnerBean() {
return "negativeOuterPositiveInnerBean";
}
}
}
static class TestMatchCondition extends SpringBootCondition implements
ConfigurationCondition {
private final ConfigurationPhase phase;
private final boolean match;
public TestMatchCondition(ConfigurationPhase phase, boolean match) {
this.phase = phase;
this.match = match;
}
@Override
public ConfigurationPhase getConfigurationPhase() {
return this.phase;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
return new ConditionOutcome(this.match, ClassUtils.getShortName(getClass()));
}
}
static class MatchParseCondition extends TestMatchCondition {
public MatchParseCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION, true);
}
}
static class MatchBeanCondition extends TestMatchCondition {
public MatchBeanCondition() {
super(ConfigurationPhase.REGISTER_BEAN, true);
}
}
static class NoMatchParseCondition extends TestMatchCondition {
public NoMatchParseCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION, false);
}
}
static class NoMatchBeanCondition extends TestMatchCondition {
public NoMatchBeanCondition() {
super(ConfigurationPhase.REGISTER_BEAN, false);
}
}
}