diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index 7d0f3370f18..f0c762369ef 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -40,11 +40,11 @@ import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; import org.springframework.boot.actuate.metrics.MetricRepository; import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.trace.TraceRepository; +import org.springframework.boot.autoconfigure.AutoConfigurationReport; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,7 +57,7 @@ import org.springframework.http.MediaType; /** * {@link EnableAutoConfiguration Auto-configuration} for common management * {@link Endpoint}s. - * + * * @author Dave Syer * @author Phillip Webb * @author Greg Turnquist diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java index 8c373b4ec09..739a2e51716 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java @@ -16,29 +16,114 @@ package org.springframework.boot.actuate.endpoint; +import java.util.List; +import java.util.Map; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; +import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint.Report; +import org.springframework.boot.autoconfigure.AutoConfigurationReport; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Condition; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; /** - * Endpoint to serve up autoconfiguration report if actuator is on the classpath. + * {@link Endpoint} to expose the {@link AutoConfigurationReport}. * * @author Greg Turnquist + * @author Phillip Webb */ @ConfigurationProperties(name = "endpoints.autoconfigurationreport", ignoreUnknownFields = false) -public class AutoConfigurationReportEndpoint extends - AbstractEndpoint { +public class AutoConfigurationReportEndpoint extends AbstractEndpoint { @Autowired private AutoConfigurationReport autoConfigurationReport; public AutoConfigurationReportEndpoint() { - super("/auto"); + super("/autoconfigurationreport"); } @Override - public AutoConfigurationReport invoke() { - return this.autoConfigurationReport; + public Report invoke() { + return new Report(this.autoConfigurationReport); } + /** + * Adapts {@link AutoConfigurationReport} to a JSON friendly structure. + */ + @JsonPropertyOrder({ "positiveMatches", "negativeMatches" }) + public static class Report { + + private MultiValueMap positiveMatches; + + private MultiValueMap negativeMatches; + + public Report(AutoConfigurationReport report) { + this.positiveMatches = new LinkedMultiValueMap(); + this.negativeMatches = new LinkedMultiValueMap(); + for (Map.Entry entry : report + .getConditionAndOutcomesBySource().entrySet()) { + dunno(entry.getValue().isFullMatch() ? this.positiveMatches + : this.negativeMatches, entry.getKey(), entry.getValue()); + + } + + } + + private void dunno(MultiValueMap map, String source, + ConditionAndOutcomes conditionAndOutcomes) { + String name = ClassUtils.getShortName(source); + for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) { + map.add(name, new MessageAndCondition(conditionAndOutcome)); + } + } + + public Map> getPositiveMatches() { + return this.positiveMatches; + } + + public Map> getNegativeMatches() { + return this.negativeMatches; + } + + } + + /** + * Adapts {@link ConditionAndOutcome} to a JSON friendly structure. + */ + @JsonPropertyOrder({ "condition", "message" }) + public static class MessageAndCondition { + + private final String condition; + + private final String message; + + public MessageAndCondition(ConditionAndOutcome conditionAndOutcome) { + Condition condition = conditionAndOutcome.getCondition(); + ConditionOutcome outcome = conditionAndOutcome.getOutcome(); + this.condition = ClassUtils.getShortName(condition.getClass()); + if (StringUtils.hasLength(outcome.getMessage())) { + this.message = outcome.getMessage(); + } + else { + this.message = (outcome.isMatch() ? "matched" : "did not match"); + } + } + + public String getCondition() { + return this.condition; + } + + public String getMessage() { + return this.message; + } + + } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java index a88cdad901a..83ccaf30ff5 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfigurationTests.java @@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure; import org.junit.Before; import org.junit.Test; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.TestUtils; import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint; import org.springframework.boot.actuate.endpoint.BeansEndpoint; @@ -29,7 +28,7 @@ import org.springframework.boot.actuate.endpoint.InfoEndpoint; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.TraceEndpoint; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; +import org.springframework.boot.autoconfigure.AutoConfigurationReport; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static org.junit.Assert.assertEquals; @@ -66,11 +65,6 @@ public class EndpointAutoConfigurationTests { assertNotNull(this.context.getBean(TraceEndpoint.class)); } - @Test(expected = NoSuchBeanDefinitionException.class) - public void noAutoConfigurationAuditEndpointByDefault() { - this.context.getBean(AutoConfigurationReportEndpoint.class); - } - @Test public void autoconfigurationAuditEndpoints() { this.context = new AnnotationConfigApplicationContext(); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java index 4a50c2e4c4e..a4bf3c57adf 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java @@ -16,38 +16,62 @@ package org.springframework.boot.actuate.endpoint; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; +import javax.annotation.PostConstruct; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint.Report; +import org.springframework.boot.autoconfigure.AutoConfigurationReport; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.Configuration; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + /** * Tests for {@link AutoConfigurationReportEndpoint}. * * @author Greg Turnquist + * @author Phillip Webb */ public class AutoConfigurationReportEndpointTests extends AbstractEndpointTests { public AutoConfigurationReportEndpointTests() { - super(Config.class, AutoConfigurationReportEndpoint.class, "/auto", true, - "endpoints.autoconfigurationreport"); + super(Config.class, AutoConfigurationReportEndpoint.class, + "/autoconfigurationreport", true, "endpoints.autoconfigurationreport"); + } + + @Test + public void invoke() throws Exception { + Report report = getEndpointBean().invoke(); + assertTrue(report.getPositiveMatches().isEmpty()); + assertTrue(report.getNegativeMatches().containsKey("a")); } @Configuration @EnableConfigurationProperties public static class Config { - @Bean - public AutoConfigurationReport autoConfigurationReport() { - return new AutoConfigurationReport(); + @Autowired + private ConfigurableApplicationContext context; + + @PostConstruct + public void setupAutoConfigurationReport() { + AutoConfigurationReport report = AutoConfigurationReport.get(this.context + .getBeanFactory()); + report.recordConditionEvaluation("a", mock(Condition.class), + mock(ConditionOutcome.class)); } @Bean public AutoConfigurationReportEndpoint endpoint() { return new AutoConfigurationReportEndpoint(); } - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReport.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReport.java new file mode 100644 index 00000000000..6a97af60697 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReport.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure; + +import java.util.ArrayList; +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.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.context.annotation.Condition; + +/** + * Records auto-configuration details for reporting and logging. + * + * @author Greg Turnquist + * @author Dave Syer + * @author Phillip Webb + */ +public class AutoConfigurationReport { + + private static final String BEAN_NAME = "autoConfigurationReport"; + + private final SortedMap outcomes = new TreeMap(); + + /** + * Private constructor. + * @see #get(ConfigurableListableBeanFactory) + */ + private AutoConfigurationReport() { + } + + /** + * Record the occurrence of condition evaluation. + * @param source the source of the condition (class or method name) + * @param condition the condition evaluated + * @param outcome the condition outcome + */ + public void recordConditionEvaluation(String source, Condition condition, + ConditionOutcome outcome) { + if (!this.outcomes.containsKey(source)) { + this.outcomes.put(source, new ConditionAndOutcomes()); + } + this.outcomes.get(source).add(condition, outcome); + } + + /** + * Returns condition outcomes from this report, grouped by the source. + */ + public Map getConditionAndOutcomesBySource() { + return Collections.unmodifiableMap(this.outcomes); + } + + /** + * Obtain a {@link AutoConfigurationReport} for the specified bean factory. + * @param beanFactory the bean factory + * @return an existing or new {@link AutoConfigurationReport} + */ + public static AutoConfigurationReport get(ConfigurableListableBeanFactory beanFactory) { + synchronized (beanFactory) { + try { + return beanFactory.getBean(BEAN_NAME, AutoConfigurationReport.class); + } + catch (NoSuchBeanDefinitionException ex) { + AutoConfigurationReport report = new AutoConfigurationReport(); + beanFactory.registerSingleton(BEAN_NAME, report); + return report; + } + } + } + + /** + * Provides access to a number of {@link ConditionAndOutcome} items. + */ + public static class ConditionAndOutcomes implements Iterable { + + private List outcomes = new ArrayList(); + + public void add(Condition condition, ConditionOutcome outcome) { + this.outcomes.add(new ConditionAndOutcome(condition, outcome)); + } + + /** + * Return {@code true} if all outcomes match. + */ + public boolean isFullMatch() { + for (ConditionAndOutcome conditionAndOutcomes : this) { + if (!conditionAndOutcomes.getOutcome().isMatch()) { + return false; + } + } + return true; + } + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.outcomes).iterator(); + } + + } + + /** + * Provides access to a single {@link Condition} and {@link ConditionOutcome}. + */ + public static class ConditionAndOutcome { + + private final Condition condition; + + private final ConditionOutcome outcome; + + public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) { + this.condition = condition; + this.outcome = outcome; + } + + public Condition getCondition() { + return this.condition; + } + + public ConditionOutcome getOutcome() { + return this.outcome; + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializer.java new file mode 100644 index 00000000000..9d2cbf0fe84 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationErrorHandler; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes; +import org.springframework.boot.logging.LogLevel; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ApplicationContextInitializer} and {@link SpringApplicationErrorHandler} that + * writes the {@link AutoConfigurationReport} to the log. Reports are logged at the + * {@link LogLevel#DEBUG DEBUG} level unless there was a problem, in which case they are + * the {@link LogLevel#INFO INFO} level is used. + * + *

+ * This initializer is not intended to be shared across multiple application context + * instances. + * + * @author Greg Turnquist + * @author Dave Syer + * @author Phillip Webb + */ +public class AutoConfigurationReportLoggingInitializer implements + ApplicationContextInitializer, + SpringApplicationErrorHandler { + + private static final String LOGGER_BEAN = "autoConfigurationReportLogger"; + + private AutoConfigurationReportLogger loggerBean; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + this.loggerBean = new AutoConfigurationReportLogger(applicationContext); + applicationContext.getBeanFactory().registerSingleton(LOGGER_BEAN, + this.loggerBean); + } + + @Override + public void handleError(SpringApplication application, + ConfigurableApplicationContext applicationContext, String[] args, + Throwable exception) { + if (this.loggerBean != null) { + this.loggerBean.logAutoConfigurationReport(true); + } + } + + /** + * Spring bean to actually perform the logging. + */ + public static class AutoConfigurationReportLogger implements + ApplicationListener { + + private final Log logger = LogFactory.getLog(getClass()); + + private final ConfigurableApplicationContext applicationContext; + + private final AutoConfigurationReport report; + + public AutoConfigurationReportLogger( + ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + // Get the report early in case the context fails to load + this.report = AutoConfigurationReport.get(this.applicationContext + .getBeanFactory()); + + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + logAutoConfigurationReport(); + } + } + + private void logAutoConfigurationReport() { + logAutoConfigurationReport(!this.applicationContext.isActive()); + } + + void logAutoConfigurationReport(boolean isCrashReport) { + if (this.report.getConditionAndOutcomesBySource().size() > 0) { + if (isCrashReport && this.logger.isInfoEnabled()) { + this.logger.info(getLogMessage(this.report + .getConditionAndOutcomesBySource())); + } + else if (!isCrashReport && this.logger.isDebugEnabled()) { + this.logger.debug(getLogMessage(this.report + .getConditionAndOutcomesBySource())); + } + } + } + + private StringBuilder getLogMessage(Map outcomes) { + StringBuilder message = new StringBuilder(); + message.append("\n\n\n"); + message.append("=========================\n"); + message.append("AUTO-CONFIGURATION REPORT\n"); + message.append("=========================\n\n\n"); + message.append("Positive matches:\n"); + message.append("-----------------\n"); + for (Map.Entry entry : outcomes.entrySet()) { + if (entry.getValue().isFullMatch()) { + addLogMessage(message, entry.getKey(), entry.getValue()); + } + } + message.append("\n\n"); + message.append("Negative matches:\n"); + message.append("-----------------\n"); + for (Map.Entry entry : outcomes.entrySet()) { + if (!entry.getValue().isFullMatch()) { + addLogMessage(message, entry.getKey(), entry.getValue()); + } + } + message.append("\n\n"); + return message; + } + + private void addLogMessage(StringBuilder message, String source, + ConditionAndOutcomes conditionAndOutcomes) { + message.append("\n " + ClassUtils.getShortName(source) + "\n"); + for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) { + message.append(" - "); + if (StringUtils.hasLength(conditionAndOutcome.getOutcome().getMessage())) { + message.append(conditionAndOutcome.getOutcome().getMessage()); + } + else { + message.append(conditionAndOutcome.getOutcome().isMatch() ? "matched" + : "did not match"); + } + message.append(" ("); + message.append(ClassUtils.getShortName(conditionAndOutcome.getCondition() + .getClass())); + message.append(")\n"); + } + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java index 01a1266b463..f1456bb0e38 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java @@ -18,7 +18,7 @@ package org.springframework.boot.autoconfigure.condition; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; +import org.springframework.boot.autoconfigure.AutoConfigurationReport; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -40,51 +40,48 @@ public abstract class SpringBootCondition implements Condition { @Override public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - ConditionOutcome result = getMatchOutcome(context, metadata); - StringBuilder message = getMessage(metadata, result); - - if (!result.isMatch()) { - // Log non-matching conditions at debug - if (this.logger.isDebugEnabled()) { - this.logger.debug(message); - } - AutoConfigurationReport.registerDecision(context, message.toString(), getClassOrMethodName(metadata), result); - return false; - } - - // Log matching conditions at trace - if (this.logger.isTraceEnabled()) { - this.logger.trace(message); - } - AutoConfigurationReport.registerDecision(context, message.toString(), getClassOrMethodName(metadata), result); - return true; + String classOrMethodName = getClassOrMethodName(metadata); + ConditionOutcome outcome = getMatchOutcome(context, metadata); + logOutcome(classOrMethodName, outcome); + recordInAutoConfigurationReport(context, classOrMethodName, outcome); + return outcome.isMatch(); } - private StringBuilder getMessage(AnnotatedTypeMetadata metadata, ConditionOutcome result) { - StringBuilder message = new StringBuilder(); - message.append("Condition "); - message.append(ClassUtils.getShortName(getClass())); - message.append(" on "); - message.append(getClassOrMethodName(metadata)); - message.append(result.isMatch() ? " matched" : " did not match"); - if (StringUtils.hasLength(result.getMessage())) { - message.append(" due to "); - message.append(result.getMessage()); - } - return message; - } - - private String getClassOrMethodName(AnnotatedTypeMetadata metadata) { + private static String getClassOrMethodName(AnnotatedTypeMetadata metadata) { if (metadata instanceof ClassMetadata) { ClassMetadata classMetadata = (ClassMetadata) metadata; return classMetadata.getClassName(); } - else if (metadata instanceof MethodMetadata) { - MethodMetadata methodMetadata = (MethodMetadata) metadata; - return methodMetadata.getDeclaringClassName() + "#" + methodMetadata.getMethodName(); + MethodMetadata methodMetadata = (MethodMetadata) metadata; + return methodMetadata.getDeclaringClassName() + "#" + + methodMetadata.getMethodName(); + } + + private void logOutcome(String classOrMethodName, ConditionOutcome outcome) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(getLogMessage(classOrMethodName, outcome)); } - else { - return ""; + } + + private StringBuilder getLogMessage(String classOrMethodName, ConditionOutcome outcome) { + StringBuilder message = new StringBuilder(); + message.append("Condition "); + message.append(ClassUtils.getShortName(getClass())); + message.append(" on "); + message.append(classOrMethodName); + message.append(outcome.isMatch() ? " matched" : " did not match"); + if (StringUtils.hasLength(outcome.getMessage())) { + message.append(" due to "); + message.append(outcome.getMessage()); + } + return message; + } + + private void recordInAutoConfigurationReport(ConditionContext context, + String classOrMethodName, ConditionOutcome outcome) { + if (context.getBeanFactory() != null) { + AutoConfigurationReport.get(context.getBeanFactory()) + .recordConditionEvaluation(classOrMethodName, this, outcome); } } @@ -94,6 +91,13 @@ public abstract class SpringBootCondition implements Condition { public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata); + /** + * Return true if any of the specified conditions match. + * @param context the context + * @param metadata the annotation meta-data + * @param conditions conditions to test + * @return {@code true} if any condition matches. + */ protected final boolean anyMatches(ConditionContext context, AnnotatedTypeMetadata metadata, Condition... conditions) { for (Condition condition : conditions) { @@ -104,6 +108,13 @@ public abstract class SpringBootCondition implements Condition { return false; } + /** + * Return true if any of the specified condition matches. + * @param context the context + * @param metadata the annotation meta-data + * @param condition condition to test + * @return {@code true} if the condition matches. + */ protected final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata, Condition condition) { if (condition instanceof SpringBootCondition) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationDecision.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationDecision.java deleted file mode 100644 index 722ab641c4d..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationDecision.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.report; - -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; - -/** - * Collects details about decision made during autoconfiguration (pass or fail) - * - * @author Greg Turnquist - */ -public class AutoConfigurationDecision { - - private final String message; - private final String classOrMethodName; - private final ConditionOutcome outcome; - - public AutoConfigurationDecision(String message, String classOrMethodName, - ConditionOutcome outcome) { - this.message = message; - this.classOrMethodName = classOrMethodName; - this.outcome = outcome; - } - - public String getMessage() { - return this.message; - } - - public String getClassOrMethodName() { - return this.classOrMethodName; - } - - public ConditionOutcome getOutcome() { - return this.outcome; - } - - @Override - public String toString() { - return "AutoConfigurationDecision{" + "message='" + this.message + '\'' - + ", classOrMethodName='" + this.classOrMethodName + '\'' + ", outcome=" - + this.outcome + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - AutoConfigurationDecision decision = (AutoConfigurationDecision) o; - - if (this.message != null ? !this.message.equals(decision.message) - : decision.message != null) - return false; - if (this.outcome != null ? !this.outcome.equals(decision.outcome) - : decision.outcome != null) - return false; - - return true; - } - - @Override - public int hashCode() { - int result = this.message != null ? this.message.hashCode() : 0; - result = 31 * result + (this.outcome != null ? this.outcome.hashCode() : 0); - return result; - } -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReport.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReport.java deleted file mode 100644 index 5003fcfbf66..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReport.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2012-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.report; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.util.ClassUtils; - -/** - * Bean used to gather auto-configuration decisions, and then generate a collection of - * info for beans that were created as well as situations where the conditional outcome - * was negative. - * - * @author Greg Turnquist - * @author Dave Syer - */ -public class AutoConfigurationReport implements ApplicationContextAware, - ApplicationListener { - - private static final String AUTO_CONFIGURATION_REPORT = "autoConfigurationReport"; - - private static Log logger = LogFactory.getLog(AutoConfigurationReport.class); - - private Set beansCreated = new LinkedHashSet(); - - private Map> autoconfigurationDecisions = new LinkedHashMap>(); - - private Map> positive = new LinkedHashMap>(); - - private Map> negative = new LinkedHashMap>(); - - private ConfigurableApplicationContext context; - - private boolean initialized = false; - - public static void registerDecision(ConditionContext context, String message, - String classOrMethodName, ConditionOutcome outcome) { - if (context.getBeanFactory().containsBeanDefinition(AUTO_CONFIGURATION_REPORT) - || context.getBeanFactory().containsSingleton(AUTO_CONFIGURATION_REPORT)) { - AutoConfigurationReport autoconfigurationReport = context.getBeanFactory() - .getBean(AUTO_CONFIGURATION_REPORT, AutoConfigurationReport.class); - autoconfigurationReport.registerDecision(message, classOrMethodName, outcome); - } - } - - public static AutoConfigurationReport registerReport( - ConfigurableApplicationContext applicationContext, - ConfigurableListableBeanFactory beanFactory) { - if (!beanFactory.containsBean(AutoConfigurationReport.AUTO_CONFIGURATION_REPORT)) { - AutoConfigurationReport report = new AutoConfigurationReport(); - report.setApplicationContext(applicationContext); - beanFactory.registerSingleton( - AutoConfigurationReport.AUTO_CONFIGURATION_REPORT, report); - } - return beanFactory.getBean(AutoConfigurationReport.AUTO_CONFIGURATION_REPORT, - AutoConfigurationReport.class); - } - - private void registerDecision(String message, String classOrMethodName, - ConditionOutcome outcome) { - AutoConfigurationDecision decision = new AutoConfigurationDecision(message, - classOrMethodName, outcome); - if (!this.autoconfigurationDecisions.containsKey(classOrMethodName)) { - this.autoconfigurationDecisions.put(classOrMethodName, - new ArrayList()); - } - this.autoconfigurationDecisions.get(classOrMethodName).add(decision); - } - - public Set getBeansCreated() { - return this.beansCreated; - } - - public Map> getNegativeDecisions() { - return this.negative; - } - - public Set> getBeanTypesCreated() { - Set> beanTypesCreated = new HashSet>(); - for (CreatedBeanInfo bootCreatedBeanInfo : this.getBeansCreated()) { - beanTypesCreated.add(bootCreatedBeanInfo.getType()); - } - return beanTypesCreated; - } - - public Set getBeanNamesCreated() { - Set beanNamesCreated = new HashSet(); - for (CreatedBeanInfo bootCreatedBeanInfo : this.getBeansCreated()) { - beanNamesCreated.add(bootCreatedBeanInfo.getName()); - } - return beanNamesCreated; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.context = (ConfigurableApplicationContext) applicationContext; - } - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - initialize(); - } - - public void initialize() { - if (!this.initialized) { - synchronized (this) { - if (!this.initialized) { - this.initialized = true; - try { - splitDecisionsIntoPositiveAndNegative(); - scanPositiveDecisionsForBeansBootCreated(); - } - finally { - if (shouldLogReport() && logger.isInfoEnabled()) { - logger.info("Created beans:"); - for (CreatedBeanInfo info : this.beansCreated) { - logger.info(info); - } - logger.info("Negative decisions:"); - for (String key : this.negative.keySet()) { - logger.info(key + ": " + this.negative.get(key)); - } - } - } - } - } - } - } - - private boolean shouldLogReport() { - String debug = this.context.getEnvironment().getProperty("debug", "false") - .toLowerCase().trim(); - return debug.equals("true") || debug.equals("") // - // inactive context is a sign that it crashed - || !this.context.isActive(); - } - - /** - * Scan the list of {@link AutoConfigurationDecision}'s, and if all outcomes true, - * then put it on the positive list. Otherwise, put it on the negative list. - */ - private synchronized void splitDecisionsIntoPositiveAndNegative() { - for (String key : this.autoconfigurationDecisions.keySet()) { - boolean match = true; - for (AutoConfigurationDecision decision : this.autoconfigurationDecisions - .get(key)) { - if (!decision.getOutcome().isMatch()) { - match = false; - } - } - if (match) { - if (!this.positive.containsKey(key)) { - this.positive.put(key, new ArrayList()); - } - for (AutoConfigurationDecision decision : this.autoconfigurationDecisions - .get(key)) { - this.positive.get(key).add(decision.getMessage()); - } - } - else { - if (!this.negative.containsKey(key)) { - this.negative.put(key, new ArrayList()); - } - for (AutoConfigurationDecision decision : this.autoconfigurationDecisions - .get(key)) { - this.negative.get(key).add(decision.getMessage()); - } - } - } - } - - /** - * Scan all the decisions based on successful outcome, and try to find the - * corresponding beans Boot created. - */ - private synchronized void scanPositiveDecisionsForBeansBootCreated() { - for (String key : this.positive.keySet()) { - for (AutoConfigurationDecision decision : this.autoconfigurationDecisions - .get(key)) { - for (String beanName : this.context.getBeanDefinitionNames()) { - Object bean = null; - if (decision.getMessage().contains(beanName) - && decision.getMessage().contains("matched")) { - try { - bean = this.context.getBean(beanName); - boolean anyMethodsAreBeans = false; - for (Method method : bean.getClass().getMethods()) { - if (this.context.containsBean(method.getName())) { - this.beansCreated.add(new CreatedBeanInfo(method - .getName(), method.getReturnType(), - this.positive.get(key))); - anyMethodsAreBeans = true; - } - } - - if (!anyMethodsAreBeans) { - this.beansCreated.add(new CreatedBeanInfo(beanName, bean - .getClass(), this.positive.get(key))); - } - } - catch (RuntimeException e) { - Class type = null; - ConfigurableApplicationContext configurable = this.context; - String beanClassName = configurable.getBeanFactory() - .getBeanDefinition(beanName).getBeanClassName(); - if (beanClassName != null) { - type = ClassUtils.resolveClassName(beanClassName, - configurable.getClassLoader()); - } - this.beansCreated.add(new CreatedBeanInfo(beanName, type, - this.positive.get(key))); - } - } - } - } - } - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReportApplicationContextInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReportApplicationContextInitializer.java deleted file mode 100644 index 2c524e97c30..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReportApplicationContextInitializer.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.report; - -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringApplicationErrorHandler; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; - -/** - * @author Dave Syer - */ -public class AutoConfigurationReportApplicationContextInitializer implements - ApplicationContextInitializer, - SpringApplicationErrorHandler { - - private AutoConfigurationReport report; - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); - this.report = AutoConfigurationReport.registerReport(applicationContext, - beanFactory); - } - - @Override - public void handleError(SpringApplication application, - ConfigurableApplicationContext applicationContext, String[] args, - Throwable exception) { - if (this.report != null) { - this.report.initialize(); // salvage a report if possible - } - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/BootCreatedBeanInfo.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/BootCreatedBeanInfo.java deleted file mode 100644 index 520da250278..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/BootCreatedBeanInfo.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.report; - -import java.util.List; - -/** - * A collection of data about a bean created by Boot - * - * @author Greg Turnquist - */ -public class BootCreatedBeanInfo { - - private final String beanName; - private final Class beanType; - private final List decisions; - - public BootCreatedBeanInfo(String beanName, Object bean, List decisions) { - this.beanName = beanName; - this.beanType = bean.getClass(); - this.decisions = decisions; - } - - public BootCreatedBeanInfo(String beanName, Class declaredBeanType, List decisions) { - this.beanName = beanName; - this.beanType = declaredBeanType; - this.decisions = decisions; - } - - @Override - public String toString() { - return "BootCreatedBeanInfo{" + "beanName='" + beanName + '\'' + ", beanType=" + beanType - + ", decisions=" + decisions + '}'; - } - - public String getBeanName() { - return beanName; - } - - public Class getBeanType() { - return beanType; - } - - public List getDecisions() { - return decisions; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - BootCreatedBeanInfo bootCreatedBeanInfo = (BootCreatedBeanInfo) o; - - if (beanName != null ? !beanName.equals(bootCreatedBeanInfo.beanName) - : bootCreatedBeanInfo.beanName != null) - return false; - - return true; - } - - @Override - public int hashCode() { - return beanName != null ? beanName.hashCode() : 0; - } -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/CreatedBeanInfo.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/CreatedBeanInfo.java deleted file mode 100644 index ef8f11f13b1..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/CreatedBeanInfo.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.report; - -import java.util.List; - -/** - * A collection of data about a bean created by Boot - * - * @author Greg Turnquist - */ -public class CreatedBeanInfo { - - private final String name; - private final Class type; - private final List decisions; - - public CreatedBeanInfo(String beanName, Class declaredBeanType, - List decisions) { - this.name = beanName; - this.type = declaredBeanType; - this.decisions = decisions; - } - - @Override - public String toString() { - return "{" + "name='" + this.name + '\'' + ", type=" + this.type + ", decisions=" - + this.decisions + '}'; - } - - public String getName() { - return this.name; - } - - public Class getType() { - return this.type; - } - - public List getDecisions() { - return this.decisions; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - CreatedBeanInfo bootCreatedBeanInfo = (CreatedBeanInfo) o; - - if (this.name != null ? !this.name.equals(bootCreatedBeanInfo.name) - : bootCreatedBeanInfo.name != null) - return false; - - return true; - } - - @Override - public int hashCode() { - return this.name != null ? this.name.hashCode() : 0; - } -} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 278a0d69283..86ecbacf01d 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -20,5 +20,6 @@ org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\ org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\ org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration + org.springframework.context.ApplicationContextInitializer=\ -org.springframework.boot.autoconfigure.report.AutoConfigurationReportApplicationContextInitializer +org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializerTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializerTests.java new file mode 100644 index 00000000000..65618d56e45 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportLoggingInitializerTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogConfigurationException; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.logging.impl.LogFactoryImpl; +import org.apache.commons.logging.impl.NoOpLog; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer.AutoConfigurationReportLogger; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutoConfigurationReportLoggingInitializer}. + * + * @author Phillip Webb + */ +public class AutoConfigurationReportLoggingInitializerTests { + + private static ThreadLocal logThreadLocal = new ThreadLocal(); + + private Log log; + + private AutoConfigurationReportLoggingInitializer initializer; + + protected List debugLog = new ArrayList(); + + protected List infoLog = new ArrayList(); + + @Before + public void setup() { + + this.log = mock(Log.class); + logThreadLocal.set(this.log); + + given(this.log.isDebugEnabled()).willReturn(true); + willAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return AutoConfigurationReportLoggingInitializerTests.this.debugLog + .add(String.valueOf(invocation.getArguments()[0])); + } + }).given(this.log).debug(anyObject()); + + given(this.log.isInfoEnabled()).willReturn(true); + willAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return AutoConfigurationReportLoggingInitializerTests.this.infoLog + .add(String.valueOf(invocation.getArguments()[0])); + } + }).given(this.log).info(anyObject()); + + LogFactory.releaseAll(); + System.setProperty(LogFactory.FACTORY_PROPERTY, MockLogFactory.class.getName()); + this.initializer = new AutoConfigurationReportLoggingInitializer(); + } + + @After + public void cleanup() { + System.clearProperty(LogFactory.FACTORY_PROPERTIES); + LogFactory.releaseAll(); + } + + @Test + public void logsDebugOnContextRefresh() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + this.initializer.initialize(context); + context.register(Config.class); + context.refresh(); + assertThat(this.debugLog.size(), not(equalTo(0))); + } + + @Test + public void logsInfoOnError() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + this.initializer.initialize(context); + context.register(ErrorConfig.class); + try { + context.refresh(); + fail("Did not error"); + } + catch (Exception ex) { + this.initializer.handleError(null, context, new String[] {}, ex); + } + + assertThat(this.debugLog.size(), equalTo(0)); + assertThat(this.infoLog.size(), not(equalTo(0))); + } + + @Test + public void logsOutput() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + this.initializer.initialize(context); + context.register(Config.class); + context.refresh(); + for (String message : this.debugLog) { + System.out.println(message); + } + // Just basic sanity check, test is for visual inspection + String l = this.debugLog.get(0); + assertThat(l, containsString("not a web application (OnWebApplicationCondition)")); + } + + public static class MockLogFactory extends LogFactoryImpl { + @Override + public Log getInstance(String name) throws LogConfigurationException { + if (AutoConfigurationReportLogger.class.getName().equals(name)) { + return logThreadLocal.get(); + } + return new NoOpLog(); + } + } + + @Configuration + @Import(WebMvcAutoConfiguration.class) + static class Config { + + } + + @Configuration + @Import(WebMvcAutoConfiguration.class) + static class ErrorConfig { + @Bean + public String iBreak() { + throw new RuntimeException(); + } + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java index 5f59bc183b8..e224ad77129 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java @@ -16,180 +16,134 @@ package org.springframework.boot.autoconfigure; -import java.util.List; +import java.util.Iterator; import java.util.Map; -import java.util.Set; +import org.junit.Before; import org.junit.Test; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; -import org.springframework.boot.autoconfigure.jms.JmsTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; -import org.springframework.boot.autoconfigure.report.AutoConfigurationReportApplicationContextInitializer; -import org.springframework.boot.autoconfigure.report.CreatedBeanInfo; -import org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcome; +import org.springframework.boot.autoconfigure.AutoConfigurationReport.ConditionAndOutcomes; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.jms.core.JmsTemplate; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.Import; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; /** * Tests for {@link AutoConfigurationReport}. * * @author Greg Turnquist + * @author Phillip Webb */ public class AutoConfigurationReportTests { - private AnnotationConfigApplicationContext context; + private DefaultListableBeanFactory beanFactory; - @Test - public void simpleReportTestCase() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(TestConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - new AutoConfigurationReportApplicationContextInitializer() - .initialize(this.context); - this.context.refresh(); - AutoConfigurationReport autoconfigSettings = this.context - .getBean(AutoConfigurationReport.class); + private AutoConfigurationReport report; - Set beansBootCreated = autoconfigSettings.getBeansCreated(); - Set beanNamesBootCreated = autoconfigSettings.getBeanNamesCreated(); - Set> beanTypesBootCreated = autoconfigSettings.getBeanTypesCreated(); + @Mock + private Condition condition1; - assertEquals(1, beansBootCreated.size()); - assertEquals(1, beanNamesBootCreated.size()); - assertEquals(1, beanTypesBootCreated.size()); + @Mock + private Condition condition2; - assertTrue(beanNamesBootCreated.contains("propertySourcesPlaceholderConfigurer")); - assertTrue(beanTypesBootCreated - .contains(PropertySourcesPlaceholderConfigurer.class)); + @Mock + private Condition condition3; - boolean foundPropertySourcesPlaceHolderConfigurer = false; - int totalDecisions = 0; - for (CreatedBeanInfo item : beansBootCreated) { - for (String decision : item.getDecisions()) { - totalDecisions += 1; - if (decision.contains("propertySourcesPlaceholderConfigurer matched")) { - foundPropertySourcesPlaceHolderConfigurer = true; - } - } - } - assertEquals(1, totalDecisions); - assertTrue(foundPropertySourcesPlaceHolderConfigurer); + @Mock + private ConditionOutcome outcome1; + + @Mock + private ConditionOutcome outcome2; + + @Mock + private ConditionOutcome outcome3; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + this.beanFactory = new DefaultListableBeanFactory(); + this.report = AutoConfigurationReport.get(this.beanFactory); } @Test - public void rabbitReportTest() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(TestConfiguration.class, RabbitAutoConfiguration.class, - AutoConfigurationReport.class); - this.context.refresh(); - AutoConfigurationReport autoconfigSettings = this.context - .getBean(AutoConfigurationReport.class); - - Set beansBootCreated = autoconfigSettings.getBeansCreated(); - Set beanNamesBootCreated = autoconfigSettings.getBeanNamesCreated(); - Set> beanTypesBootCreated = autoconfigSettings.getBeanTypesCreated(); - - assertEquals(3, beansBootCreated.size()); - assertEquals(3, beanNamesBootCreated.size()); - assertEquals(3, beanTypesBootCreated.size()); - - assertTrue(beanNamesBootCreated.contains("amqpAdmin")); - assertTrue(beanNamesBootCreated.contains("rabbitConnectionFactory")); - assertTrue(beanNamesBootCreated.contains("rabbitTemplate")); - - assertTrue(beanTypesBootCreated.contains(RabbitAdmin.class)); - assertTrue(beanTypesBootCreated.contains(ConnectionFactory.class)); - assertTrue(beanTypesBootCreated.contains(RabbitTemplate.class)); - - boolean foundRabbitConnectionFactory = false; - boolean foundAmqpAdminExpressionCondition = false; - boolean foundAmqpAdminBeanCondition = false; - boolean foundRabbitTemplateCondition = false; - int totalDecisions = 0; - for (CreatedBeanInfo item : beansBootCreated) { - for (String decision : item.getDecisions()) { - totalDecisions += 1; - if (decision.contains("RabbitConnectionFactoryCreator matched")) { - foundRabbitConnectionFactory = true; - } - else if (decision.contains("OnExpressionCondition") - && decision.contains("amqpAdmin matched due to SpEL expression")) { - foundAmqpAdminExpressionCondition = true; - } - else if (decision.contains("OnBeanCondition") - && decision.contains("amqpAdmin matched")) { - foundAmqpAdminBeanCondition = true; - } - else if (decision.contains("rabbitTemplate matched")) { - foundRabbitTemplateCondition = true; - } - } - } - assertEquals(4, totalDecisions); - assertTrue(foundRabbitConnectionFactory); - assertTrue(foundAmqpAdminExpressionCondition); - assertTrue(foundAmqpAdminBeanCondition); - assertTrue(foundRabbitTemplateCondition); + public void get() throws Exception { + assertThat(this.report, not(nullValue())); + assertThat(this.report, + sameInstance(AutoConfigurationReport.get(this.beanFactory))); } @Test - public void verifyItGathersNegativeMatches() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(TestConfiguration2.class, - JmsTemplateAutoConfiguration.class, MultipartAutoConfiguration.class, - AutoConfigurationReport.class); - this.context.refresh(); - AutoConfigurationReport autoconfigSettings = this.context - .getBean(AutoConfigurationReport.class); - - Map> negatives = autoconfigSettings.getNegativeDecisions(); - - boolean foundMyOwnJmsTemplateAndBackedOff = false; - boolean didNotFindMultipartConfigElement = false; - int totalNegativeDecisions = 0; - for (String key : negatives.keySet()) { - for (String decision : negatives.get(key)) { - totalNegativeDecisions += 1; - if (decision - .contains("JmsTemplateAutoConfiguration#jmsTemplate did not match") - && decision.contains("found the following [myOwnJmsTemplate]")) { - foundMyOwnJmsTemplateAndBackedOff = true; - } - else if (decision.contains("MultipartAutoConfiguration did not match") - && decision - .contains("list['javax.servlet.MultipartConfigElement']") - && decision.contains("found no beans")) { - didNotFindMultipartConfigElement = true; - } - } - } - // varying situations might cause multi-conditional beans to evaluate in different - // orders - assertTrue(totalNegativeDecisions >= 2); - assertTrue(foundMyOwnJmsTemplateAndBackedOff); - assertTrue(didNotFindMultipartConfigElement); + public void recordConditionEvaluations() throws Exception { + this.report.recordConditionEvaluation("a", this.condition1, this.outcome1); + this.report.recordConditionEvaluation("a", this.condition2, this.outcome2); + this.report.recordConditionEvaluation("b", this.condition3, this.outcome3); + Map map = this.report + .getConditionAndOutcomesBySource(); + assertThat(map.size(), equalTo(2)); + Iterator iterator = map.get("a").iterator(); + ConditionAndOutcome conditionAndOutcome = iterator.next(); + assertThat(conditionAndOutcome.getCondition(), equalTo(this.condition1)); + assertThat(conditionAndOutcome.getOutcome(), equalTo(this.outcome1)); + conditionAndOutcome = iterator.next(); + assertThat(conditionAndOutcome.getCondition(), equalTo(this.condition2)); + assertThat(conditionAndOutcome.getOutcome(), equalTo(this.outcome2)); + assertThat(iterator.hasNext(), equalTo(false)); + iterator = map.get("b").iterator(); + conditionAndOutcome = iterator.next(); + assertThat(conditionAndOutcome.getCondition(), equalTo(this.condition3)); + assertThat(conditionAndOutcome.getOutcome(), equalTo(this.outcome3)); + assertThat(iterator.hasNext(), equalTo(false)); } - @Configuration - public static class TestConfiguration { + @Test + public void fullMatch() throws Exception { + prepareMatches(true, true, true); + assertThat(this.report.getConditionAndOutcomesBySource().get("a").isFullMatch(), + equalTo(true)); } - @Configuration - public static class TestConfiguration2 { - @Bean - JmsTemplate myOwnJmsTemplate(javax.jms.ConnectionFactory connectionFactory) { - return new JmsTemplate(connectionFactory); - } + @Test + public void notFullMatch() throws Exception { + prepareMatches(true, false, true); + assertThat(this.report.getConditionAndOutcomesBySource().get("a").isFullMatch(), + equalTo(false)); + } + + private void prepareMatches(boolean m1, boolean m2, boolean m3) { + given(this.outcome1.isMatch()).willReturn(m1); + given(this.outcome2.isMatch()).willReturn(m2); + given(this.outcome3.isMatch()).willReturn(m3); + this.report.recordConditionEvaluation("a", this.condition1, this.outcome1); + this.report.recordConditionEvaluation("a", this.condition2, this.outcome2); + this.report.recordConditionEvaluation("a", this.condition3, this.outcome3); + } + + @Test + @SuppressWarnings("resource") + public void springBootConditionPopulatesReport() throws Exception { + AutoConfigurationReport report = AutoConfigurationReport + .get(new AnnotationConfigApplicationContext(Config.class) + .getBeanFactory()); + assertThat(report.getConditionAndOutcomesBySource().size(), not(equalTo(0))); + } + + @Configurable + @Import(WebMvcAutoConfiguration.class) + static class Config { + } }