From b63016d8fcb4703400ea864555f9ec9901acef20 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Fri, 1 Nov 2013 23:01:54 -0500 Subject: [PATCH] Create a report based on Boot's autoconfiguration decisions - Gather autoconfiguration conditional decisiions (true and false) - Provide an actuator endpoint as one means to read the report - Define @EnableAutConfigurationReport annotation to turn this feature on - Tidy up autoconfig report a bit and log it if --debug=true --- .../EndpointAutoConfiguration.java | 14 +- .../ErrorMvcAutoConfiguration.java | 1 + .../AutoConfigurationReportEndpoint.java | 43 ++++ .../EndpointAutoConfigurationTests.java | 19 +- .../AutoConfigurationReportEndpointTests.java | 48 +++++ .../boot/autoconfigure/condition/Outcome.java | 52 +++++ .../condition/SpringBootCondition.java | 70 ++---- .../jdbc/DataSourceAutoConfiguration.java | 1 + .../report/AutoConfigurationDecision.java | 81 +++++++ .../report/AutoConfigurationReport.java | 202 ++++++++++++++++++ .../report/BootCreatedBeanInfo.java | 82 +++++++ .../autoconfigure/report/CreatedBeanInfo.java | 83 +++++++ .../DispatcherServletAutoConfiguration.java | 1 + .../AutoConfigurationReportTests.java | 188 ++++++++++++++++ .../ops/ui/SampleSecureApplication.java | 2 + .../src/main/resources/application.properties | 3 +- 16 files changed, 839 insertions(+), 51 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/Outcome.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationDecision.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReport.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/BootCreatedBeanInfo.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/CreatedBeanInfo.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java 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 fb3c1d88bdf..7d0f3370f18 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 @@ -22,6 +22,7 @@ import java.util.Properties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint; import org.springframework.boot.actuate.endpoint.BeansEndpoint; import org.springframework.boot.actuate.endpoint.DumpEndpoint; import org.springframework.boot.actuate.endpoint.Endpoint; @@ -41,7 +42,9 @@ import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.trace.TraceRepository; 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.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; @@ -54,9 +57,10 @@ import org.springframework.http.MediaType; /** * {@link EnableAutoConfiguration Auto-configuration} for common management * {@link Endpoint}s. - * + * * @author Dave Syer * @author Phillip Webb + * @author Greg Turnquist */ @Configuration @ConditionalOnClass(MediaType.class) @@ -128,6 +132,13 @@ public class EndpointAutoConfiguration { return new DumpEndpoint(); } + @Bean + @ConditionalOnBean(AutoConfigurationReport.class) + @ConditionalOnMissingBean + public AutoConfigurationReportEndpoint autoConfigurationAuditEndpoint() { + return new AutoConfigurationReportEndpoint(); + } + @Bean @ConditionalOnMissingBean public ShutdownEndpoint shutdownEndpoint() { @@ -139,7 +150,6 @@ public class EndpointAutoConfiguration { @Autowired private ConfigurableEnvironment environment = new StandardEnvironment(); - @Value("${spring.git.properties:classpath:git.properties}") private Resource gitProperties; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java index 544a663fe79..d8068a23203 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.Outcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; 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 new file mode 100644 index 00000000000..9018185ae1c --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpoint.java @@ -0,0 +1,43 @@ +/* + * 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.actuate.endpoint; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.report.AutoConfigurationReport; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Endpoint to serve up autoconfiguration report if actuator is on the classpath. + * + * @author Greg Turnquist + */ +@ConfigurationProperties(name = "endpoints.autoconfigurationreport", ignoreUnknownFields = false) +public class AutoConfigurationReportEndpoint extends AbstractEndpoint { + + @Autowired + private AutoConfigurationReport autoConfigurationReport; + + public AutoConfigurationReportEndpoint() { + super("/autoconfigurationreport"); + } + + @Override + public AutoConfigurationReport invoke() { + return this.autoConfigurationReport; + } + +} 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 b3a38bb11a9..e1576235032 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,8 +18,9 @@ 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.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.AutoConfigurationReportEndpoint; import org.springframework.boot.actuate.endpoint.BeansEndpoint; import org.springframework.boot.actuate.endpoint.DumpEndpoint; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; @@ -28,6 +29,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.AutoConfigurationReportCreator; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static org.junit.Assert.assertEquals; @@ -39,6 +41,7 @@ import static org.junit.Assert.assertNull; * * @author Dave Syer * @author Phillip Webb + * @author Greg Turnquist */ public class EndpointAutoConfigurationTests { @@ -63,6 +66,20 @@ 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(); + this.context.register(EndpointAutoConfiguration.class, AutoConfigurationReportCreator.class); + this.context.refresh(); + assertNotNull(this.context.getBean(AutoConfigurationReportEndpoint.class)); + } + + @Test public void testInfoEndpointConfiguration() throws Exception { 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 new file mode 100644 index 00000000000..0c06c8be252 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/AutoConfigurationReportEndpointTests.java @@ -0,0 +1,48 @@ +/* + * 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.actuate.endpoint; + +import org.springframework.boot.autoconfigure.report.EnableAutoConfigurationReport; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Tests for {@link AutoConfigurationReportEndpoint}. + * + * @author Greg Turnquist + */ +public class AutoConfigurationReportEndpointTests extends AbstractEndpointTests { + + public AutoConfigurationReportEndpointTests() { + super(Config.class, AutoConfigurationReportEndpoint.class, + "/autoconfigurationreport", true, "endpoints.autoconfigurationreport"); + } + + @Configuration + @EnableConfigurationProperties + @EnableAutoConfigurationReport + public static class Config { + + @Bean + public AutoConfigurationReportEndpoint endpoint() { + return new AutoConfigurationReportEndpoint(); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/Outcome.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/Outcome.java new file mode 100644 index 00000000000..073b6add94f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/Outcome.java @@ -0,0 +1,52 @@ +/* + * 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.condition; + +/** + * Outcome for a match, including log message. + */ +public class Outcome { + + private final boolean match; + private final String message; + + public Outcome(boolean match, String message) { + this.match = match; + this.message = message; + } + + public static Outcome match() { + return match(null); + } + + public static Outcome match(String message) { + return new Outcome(true, message); + } + + public static Outcome noMatch(String message) { + return new Outcome(false, message); + } + + public boolean isMatch() { + return this.match; + } + + public String getMessage() { + return this.message; + } + +} 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 d557d963ca5..f56993d2abc 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,6 +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.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -31,6 +32,7 @@ import org.springframework.util.StringUtils; * logging to help the user diagnose what classes are loaded. * * @author Phillip Webb + * @author Greg Turnquist */ public abstract class SpringBootCondition implements Condition { @@ -39,18 +41,22 @@ public abstract class SpringBootCondition implements Condition { @Override public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Outcome 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(getMessage(metadata, result)); + 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(getMessage(metadata, result)); + this.logger.trace(message); } + AutoConfigurationReport.registerDecision(context, message.toString(), getClassOrMethodName(metadata), result); return true; } @@ -59,16 +65,7 @@ public abstract class SpringBootCondition implements Condition { message.append("Condition "); message.append(ClassUtils.getShortName(getClass())); message.append(" on "); - if (metadata instanceof ClassMetadata) { - ClassMetadata classMetadata = (ClassMetadata) metadata; - message.append(classMetadata.getClassName()); - } - else if (metadata instanceof MethodMetadata) { - MethodMetadata methodMetadata = (MethodMetadata) metadata; - message.append(methodMetadata.getDeclaringClassName()); - message.append("#"); - message.append(methodMetadata.getMethodName()); - } + message.append(getClassOrMethodName(metadata)); message.append(result.isMatch() ? " matched" : " did not match"); if (StringUtils.hasLength(result.getMessage())) { message.append(" due to "); @@ -77,6 +74,20 @@ public abstract class SpringBootCondition implements Condition { return message; } + private 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(); + } + else { + return ""; + } + } + /** * Determine the outcome of the match along with suitable log output. */ @@ -102,39 +113,4 @@ public abstract class SpringBootCondition implements Condition { return condition.matches(context, metadata); } - /** - * Outcome for a match, including log message. - */ - protected final static class Outcome { - - private final boolean match; - - private final String message; - - public Outcome(boolean match, String message) { - this.match = match; - this.message = message; - } - - public boolean isMatch() { - return this.match; - } - - public String getMessage() { - return this.message; - } - - public static Outcome match() { - return match(null); - } - - public static Outcome match(String message) { - return new Outcome(true, message); - } - - public static Outcome noMatch(String message) { - return new Outcome(false, message); - } - - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index 99361fc2aa1..ec08c330be3 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.Outcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.ApplicationContext; 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 new file mode 100644 index 00000000000..683363e031b --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationDecision.java @@ -0,0 +1,81 @@ +/* + * 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.Outcome; + +/** + * 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 Outcome outcome; + + public AutoConfigurationDecision(String message, String classOrMethodName, Outcome outcome) { + this.message = message; + this.classOrMethodName = classOrMethodName; + this.outcome = outcome; + } + + public String getMessage() { + return message; + } + + public String getClassOrMethodName() { + return classOrMethodName; + } + + public Outcome getOutcome() { + return outcome; + } + + @Override + public String toString() { + return "AutoConfigurationDecision{" + "message='" + message + '\'' + + ", classOrMethodName='" + classOrMethodName + '\'' + ", outcome=" + + 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 (message != null ? !message.equals(decision.message) + : decision.message != null) + return false; + if (outcome != null ? !outcome.equals(decision.outcome) + : decision.outcome != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (outcome != null ? 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 new file mode 100644 index 00000000000..0fdbc5c6c83 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/AutoConfigurationReport.java @@ -0,0 +1,202 @@ +/* + * 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.boot.autoconfigure.condition.Outcome; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.event.ContextRefreshedEvent; + +/** + * Bean used to gather autoconfiguration 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 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 ApplicationContext context; + private boolean initialized = false; + + public static void registerDecision(ConditionContext context, String message, + String classOrMethodName, Outcome outcome) { + if (context.getBeanFactory().containsBeanDefinition("autoConfigurationReport")) { + AutoConfigurationReport autoconfigurationReport = context.getBeanFactory() + .getBean(AutoConfigurationReport.class); + autoconfigurationReport.registerDecision(message, classOrMethodName, outcome); + } + } + + private void registerDecision(String message, String classOrMethodName, + Outcome 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.getBeanType()); + } + 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 = applicationContext; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + initialize(); + } + + public void initialize() { + if (!this.initialized) { + synchronized (this) { + if (!this.initialized) { + this.initialized = true; + splitDecisionsIntoPositiveAndNegative(); + scanPositiveDecisionsForBeansBootCreated(); + if (this.context.getEnvironment().getProperty("debug", Boolean.class, + false)) { + 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)); + } + } + } + } + } + } + + /** + * 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 = this.context.getBean(beanName); + if (decision.getMessage().contains(beanName) + && decision.getMessage().contains("matched")) { + 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, + this.positive.get(key))); + } + } + } + } + } + } + +} 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 new file mode 100644 index 00000000000..520da250278 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/BootCreatedBeanInfo.java @@ -0,0 +1,82 @@ +/* + * 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 new file mode 100644 index 00000000000..fbb19eee7a8 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/report/CreatedBeanInfo.java @@ -0,0 +1,83 @@ +/* + * 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, Object bean, List decisions) { + this.name = beanName; + this.type = bean.getClass(); + this.decisions = 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 getBeanType() { + 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/java/org/springframework/boot/autoconfigure/web/DispatcherServletAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DispatcherServletAutoConfiguration.java index 72fa0ace5ac..d172095d232 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DispatcherServletAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/DispatcherServletAutoConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.Outcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.context.annotation.Bean; 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 new file mode 100644 index 00000000000..16c0df15301 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReportTests.java @@ -0,0 +1,188 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.CreatedBeanInfo; +import org.springframework.boot.autoconfigure.report.EnableAutoConfigurationReport; +import org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration; +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; + +/** + * Tests for {@link AutoConfigurationReport}. + * + * @author Greg Turnquist + */ +public class AutoConfigurationReportTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void simpleReportTestCase() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + AutoConfigurationReport autoconfigSettings = this.context + .getBean(AutoConfigurationReport.class); + + Set beansBootCreated = autoconfigSettings.getBeansCreated(); + Set beanNamesBootCreated = autoconfigSettings.getBeanNamesCreated(); + Set> beanTypesBootCreated = autoconfigSettings.getBeanTypesCreated(); + + assertEquals(1, beansBootCreated.size()); + assertEquals(1, beanNamesBootCreated.size()); + assertEquals(1, beanTypesBootCreated.size()); + + assertTrue(beanNamesBootCreated.contains("propertySourcesPlaceholderConfigurer")); + assertTrue(beanTypesBootCreated + .contains(PropertySourcesPlaceholderConfigurer.class)); + + 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); + } + + @Test + public void rabbitReportTest() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, RabbitAutoConfiguration.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); + } + + @Test + public void verifyItGathersNegativeMatches() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration2.class, + JmsTemplateAutoConfiguration.class, MultipartAutoConfiguration.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); + + } + + @Configuration + @EnableAutoConfigurationReport + public static class TestConfiguration { + } + + @Configuration + @EnableAutoConfigurationReport + public static class TestConfiguration2 { + @Bean + JmsTemplate myOwnJmsTemplate(javax.jms.ConnectionFactory connectionFactory) { + return new JmsTemplate(connectionFactory); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/org/springframework/boot/sample/ops/ui/SampleSecureApplication.java b/spring-boot-samples/spring-boot-sample-secure/src/main/java/org/springframework/boot/sample/ops/ui/SampleSecureApplication.java index 672bc5baf1f..c6817a743b9 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/main/java/org/springframework/boot/sample/ops/ui/SampleSecureApplication.java +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/java/org/springframework/boot/sample/ops/ui/SampleSecureApplication.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.Map; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.report.EnableAutoConfigurationReport; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -33,6 +34,7 @@ import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @EnableAutoConfiguration +@EnableAutoConfigurationReport @ComponentScan @Controller public class SampleSecureApplication extends WebMvcConfigurerAdapter { diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure/src/main/resources/application.properties index 760bdc35a25..03e8cdc8418 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/resources/application.properties @@ -1 +1,2 @@ -spring.thymeleaf.cache: false \ No newline at end of file +spring.thymeleaf.cache: false +debug: true \ No newline at end of file