Resolves #127: Prevent duplicate report outcomes
The collection of outcomes is a list. Sometimes a race condition causes to instances of the same outcome to get added to the list shown in the report. By replacing this with a set and propery equals/hashCode, duplicates are prevented from appearing in the report. I added test cases to prove that that POJO is properly managed inside a Set and also to show that duplicates don't appear in the final report.
This commit is contained in:
parent
c71322a0b2
commit
0610378d2f
|
|
@ -16,13 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure;
|
package org.springframework.boot.autoconfigure;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.SortedMap;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.BeanFactory;
|
import org.springframework.beans.factory.BeanFactory;
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
|
|
@ -40,9 +34,7 @@ import org.springframework.context.annotation.Condition;
|
||||||
public class AutoConfigurationReport {
|
public class AutoConfigurationReport {
|
||||||
|
|
||||||
private static final String BEAN_NAME = "autoConfigurationReport";
|
private static final String BEAN_NAME = "autoConfigurationReport";
|
||||||
|
|
||||||
private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>();
|
private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>();
|
||||||
|
|
||||||
private AutoConfigurationReport parent;
|
private AutoConfigurationReport parent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,7 +107,7 @@ public class AutoConfigurationReport {
|
||||||
*/
|
*/
|
||||||
public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> {
|
public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> {
|
||||||
|
|
||||||
private List<ConditionAndOutcome> outcomes = new ArrayList<ConditionAndOutcome>();
|
private Set<ConditionAndOutcome> outcomes = new HashSet<ConditionAndOutcome>();
|
||||||
|
|
||||||
public void add(Condition condition, ConditionOutcome outcome) {
|
public void add(Condition condition, ConditionOutcome outcome) {
|
||||||
this.outcomes.add(new ConditionAndOutcome(condition, outcome));
|
this.outcomes.add(new ConditionAndOutcome(condition, outcome));
|
||||||
|
|
@ -135,7 +127,7 @@ public class AutoConfigurationReport {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<ConditionAndOutcome> iterator() {
|
public Iterator<ConditionAndOutcome> iterator() {
|
||||||
return Collections.unmodifiableList(this.outcomes).iterator();
|
return Collections.unmodifiableSet(this.outcomes).iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +138,6 @@ public class AutoConfigurationReport {
|
||||||
public static class ConditionAndOutcome {
|
public static class ConditionAndOutcome {
|
||||||
|
|
||||||
private final Condition condition;
|
private final Condition condition;
|
||||||
|
|
||||||
private final ConditionOutcome outcome;
|
private final ConditionOutcome outcome;
|
||||||
|
|
||||||
public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) {
|
public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) {
|
||||||
|
|
@ -161,6 +152,31 @@ public class AutoConfigurationReport {
|
||||||
public ConditionOutcome getOutcome() {
|
public ConditionOutcome getOutcome() {
|
||||||
return this.outcome;
|
return this.outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.outcome == null || this.outcome.getMessage() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConditionAndOutcome that = (ConditionAndOutcome) o;
|
||||||
|
|
||||||
|
if (that.getOutcome() == null || this.getOutcome().getMessage() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getOutcome().getMessage().equals(that.getOutcome().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return outcome != null && outcome.getMessage() != null ? outcome.getMessage().hashCode() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure;
|
package org.springframework.boot.autoconfigure;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -29,17 +31,19 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome;
|
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes;
|
import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||||
|
import org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
import org.springframework.context.annotation.Condition;
|
import org.springframework.context.annotation.Condition;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
|
||||||
import static org.hamcrest.Matchers.sameInstance;
|
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link AutoConfigurationReport}.
|
* Tests for {@link AutoConfigurationReport}.
|
||||||
|
|
@ -88,8 +92,7 @@ public class AutoConfigurationReportTests {
|
||||||
@Test
|
@Test
|
||||||
public void parent() throws Exception {
|
public void parent() throws Exception {
|
||||||
this.beanFactory.setParentBeanFactory(new DefaultListableBeanFactory());
|
this.beanFactory.setParentBeanFactory(new DefaultListableBeanFactory());
|
||||||
AutoConfigurationReport.get((ConfigurableListableBeanFactory) this.beanFactory
|
AutoConfigurationReport.get((ConfigurableListableBeanFactory) this.beanFactory.getParentBeanFactory());
|
||||||
.getParentBeanFactory());
|
|
||||||
assertThat(this.report,
|
assertThat(this.report,
|
||||||
sameInstance(AutoConfigurationReport.get(this.beanFactory)));
|
sameInstance(AutoConfigurationReport.get(this.beanFactory)));
|
||||||
assertThat(this.report, not(nullValue()));
|
assertThat(this.report, not(nullValue()));
|
||||||
|
|
@ -98,12 +101,15 @@ public class AutoConfigurationReportTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void recordConditionEvaluations() throws Exception {
|
public void recordConditionEvaluations() throws Exception {
|
||||||
|
given(this.outcome1.getMessage()).willReturn("Message 1");
|
||||||
|
given(this.outcome2.getMessage()).willReturn("Message 2");
|
||||||
|
given(this.outcome3.getMessage()).willReturn("Message 3");
|
||||||
|
|
||||||
this.report.recordConditionEvaluation("a", this.condition1, this.outcome1);
|
this.report.recordConditionEvaluation("a", this.condition1, this.outcome1);
|
||||||
this.report.recordConditionEvaluation("a", this.condition2, this.outcome2);
|
this.report.recordConditionEvaluation("a", this.condition2, this.outcome2);
|
||||||
this.report.recordConditionEvaluation("b", this.condition3, this.outcome3);
|
this.report.recordConditionEvaluation("b", this.condition3, this.outcome3);
|
||||||
|
|
||||||
Map<String, ConditionAndOutcomes> map = this.report
|
Map<String, ConditionAndOutcomes> map = this.report.getConditionAndOutcomesBySource();
|
||||||
.getConditionAndOutcomesBySource();
|
|
||||||
assertThat(map.size(), equalTo(2));
|
assertThat(map.size(), equalTo(2));
|
||||||
Iterator<ConditionAndOutcome> iterator = map.get("a").iterator();
|
Iterator<ConditionAndOutcome> iterator = map.get("a").iterator();
|
||||||
ConditionAndOutcome conditionAndOutcome = iterator.next();
|
ConditionAndOutcome conditionAndOutcome = iterator.next();
|
||||||
|
|
@ -146,16 +152,102 @@ public class AutoConfigurationReportTests {
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
public void springBootConditionPopulatesReport() throws Exception {
|
public void springBootConditionPopulatesReport() throws Exception {
|
||||||
AutoConfigurationReport report = AutoConfigurationReport
|
AutoConfigurationReport report = AutoConfigurationReport.get(new AnnotationConfigApplicationContext(
|
||||||
.get(new AnnotationConfigApplicationContext(Config.class)
|
Config.class).getBeanFactory());
|
||||||
.getBeanFactory());
|
|
||||||
assertThat(report.getConditionAndOutcomesBySource().size(), not(equalTo(0)));
|
assertThat(report.getConditionAndOutcomesBySource().size(), not(equalTo(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDuplicateConditionAndOutcomes() {
|
||||||
|
Condition condition1 = mock(Condition.class);
|
||||||
|
ConditionOutcome conditionOutcome1 = mock(ConditionOutcome.class);
|
||||||
|
given(conditionOutcome1.getMessage()).willReturn("This is message 1");
|
||||||
|
|
||||||
|
Condition condition2 = mock(Condition.class);
|
||||||
|
ConditionOutcome conditionOutcome2 = mock(ConditionOutcome.class);
|
||||||
|
given(conditionOutcome2.getMessage()).willReturn("This is message 2");
|
||||||
|
|
||||||
|
Condition condition3 = mock(Condition.class);
|
||||||
|
ConditionOutcome conditionOutcome3 = mock(ConditionOutcome.class);
|
||||||
|
given(conditionOutcome3.getMessage()).willReturn("This is message 2"); // identical in value to #2
|
||||||
|
|
||||||
|
ConditionAndOutcome outcome1 = new ConditionAndOutcome(condition1,
|
||||||
|
conditionOutcome1);
|
||||||
|
assertThat(outcome1, equalTo(outcome1));
|
||||||
|
|
||||||
|
ConditionAndOutcome outcome2 = new ConditionAndOutcome(condition2,
|
||||||
|
conditionOutcome2);
|
||||||
|
assertThat(outcome1, not(equalTo(outcome2)));
|
||||||
|
|
||||||
|
ConditionAndOutcome outcome3 = new ConditionAndOutcome(condition3,
|
||||||
|
conditionOutcome3);
|
||||||
|
assertThat(outcome2, equalTo(outcome3));
|
||||||
|
|
||||||
|
Set<ConditionAndOutcome> set = new HashSet<ConditionAndOutcome>();
|
||||||
|
set.add(outcome1);
|
||||||
|
set.add(outcome2);
|
||||||
|
set.add(outcome3);
|
||||||
|
assertThat(set.size(), equalTo(2));
|
||||||
|
|
||||||
|
ConditionAndOutcomes outcomes = new ConditionAndOutcomes();
|
||||||
|
outcomes.add(condition1, conditionOutcome1);
|
||||||
|
outcomes.add(condition2, conditionOutcome2);
|
||||||
|
outcomes.add(condition3, conditionOutcome3);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
Iterator<ConditionAndOutcome> iterator = outcomes.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
i++;
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
assertThat(i, equalTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void duplicateOutcomes() {
|
||||||
|
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||||
|
DuplicateConfig.class);
|
||||||
|
AutoConfigurationReport report = AutoConfigurationReport.get(context.getBeanFactory());
|
||||||
|
String autoconfigKey = "org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration";
|
||||||
|
|
||||||
|
assertThat(report.getConditionAndOutcomesBySource().keySet(),
|
||||||
|
hasItem(autoconfigKey));
|
||||||
|
|
||||||
|
ConditionAndOutcomes conditionAndOutcomes = report.getConditionAndOutcomesBySource().get(
|
||||||
|
autoconfigKey);
|
||||||
|
Iterator<ConditionAndOutcome> iterator = conditionAndOutcomes.iterator();
|
||||||
|
int i = 0;
|
||||||
|
boolean foundConditionalOnClass = false;
|
||||||
|
boolean foundConditionalOnBean = false;
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
ConditionAndOutcome conditionAndOutcome = iterator.next();
|
||||||
|
if (conditionAndOutcome.getOutcome().getMessage().contains(
|
||||||
|
"@ConditionalOnClass classes found: javax.servlet.Servlet,org.springframework.web.multipart.support.StandardServletMultipartResolver")) {
|
||||||
|
foundConditionalOnClass = true;
|
||||||
|
}
|
||||||
|
if (conditionAndOutcome.getOutcome().getMessage().contains(
|
||||||
|
"@ConditionalOnBean (types: javax.servlet.MultipartConfigElement; SearchStrategy: all) found no beans")) {
|
||||||
|
foundConditionalOnBean = true;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(i, equalTo(2));
|
||||||
|
assertTrue(foundConditionalOnClass);
|
||||||
|
assertTrue(foundConditionalOnBean);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configurable
|
@Configurable
|
||||||
@Import(WebMvcAutoConfiguration.class)
|
@Import(WebMvcAutoConfiguration.class)
|
||||||
static class Config {
|
static class Config {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configurable
|
||||||
|
@Import(MultipartAutoConfiguration.class)
|
||||||
|
static class DuplicateConfig {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,4 +216,5 @@ public abstract class AbstractJpaAutoConfigurationTests {
|
||||||
|
|
||||||
static class CustomJpaTransactionManager extends JpaTransactionManager {
|
static class CustomJpaTransactionManager extends JpaTransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package org.springframework.boot.context.embedded;
|
||||||
|
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
@ -99,7 +98,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
|
||||||
this.container = factory
|
this.container = factory
|
||||||
.getEmbeddedServletContainer(exampleServletRegistration());
|
.getEmbeddedServletContainer(exampleServletRegistration());
|
||||||
this.container.start();
|
this.container.start();
|
||||||
this.thrown.expect(SocketException.class);
|
this.thrown.expect(IOException.class);
|
||||||
getResponse("http://localhost:8080/hello");
|
getResponse("http://localhost:8080/hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +109,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
|
||||||
.getEmbeddedServletContainer(exampleServletRegistration());
|
.getEmbeddedServletContainer(exampleServletRegistration());
|
||||||
this.container.start();
|
this.container.start();
|
||||||
this.container.stop();
|
this.container.stop();
|
||||||
this.thrown.expect(SocketException.class);
|
this.thrown.expect(IOException.class);
|
||||||
getResponse("http://localhost:8080/hello");
|
getResponse("http://localhost:8080/hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue