diff --git a/spring-boot/src/main/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializer.java b/spring-boot/src/main/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializer.java new file mode 100644 index 00000000000..cf121ca72ca --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializer.java @@ -0,0 +1,179 @@ +/* + * Copyright 2012-2014 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.context; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ApplicationContextInitializer} to report warnings for common misconfiguration + * mistakes. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class ConfigurationWarningsApplicationContextInitializer implements + ApplicationContextInitializer { + + private static Log logger = LogFactory + .getLog(ConfigurationWarningsApplicationContextInitializer.class); + + @Override + public void initialize(ConfigurableApplicationContext context) { + context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor( + getChecks())); + } + + /** + * Returns the checks that should be applied. + * @return the checks to apply + */ + protected Check[] getChecks() { + return new Check[] { new ComponentScanDefaultPackageCheck() }; + } + + /** + * {@link BeanDefinitionRegistryPostProcessor} to report warnings. + */ + protected final static class ConfigurationWarningsPostProcessor implements + PriorityOrdered, BeanDefinitionRegistryPostProcessor { + + private Check[] checks; + + public ConfigurationWarningsPostProcessor(Check[] checks) { + this.checks = checks; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 1; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + for (Check check : this.checks) { + String message = check.getWarning(registry); + if (StringUtils.hasLength(message)) { + warn(message); + } + } + + } + + private void warn(String message) { + if (logger.isWarnEnabled()) { + logger.warn("\n\n** WARNING ** : " + message + "\n\n"); + } + } + + } + + /** + * A single check that can be applied. + */ + protected static interface Check { + + /** + * Returns a warning if the check fails or {@code null} if there are no problems. + * @param registry the {@link BeanDefinitionRegistry} + * @return a warning message or {@code null} + */ + String getWarning(BeanDefinitionRegistry registry); + + } + + /** + * {@link Check} for {@code @ComponentScan} on the default package. + */ + protected static class ComponentScanDefaultPackageCheck implements Check { + + @Override + public String getWarning(BeanDefinitionRegistry registry) { + if (isComponentScanningDefaultPackage(registry)) { + return "Your ApplicationContext is unlikely to start due to a " + + "@ComponentScan of the default package."; + } + return null; + } + + private boolean isComponentScanningDefaultPackage(BeanDefinitionRegistry registry) { + String[] names = registry.getBeanDefinitionNames(); + for (String name : names) { + BeanDefinition definition = registry.getBeanDefinition(name); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annotatedDefinition = (AnnotatedBeanDefinition) definition; + if (isScanningDefaultPackage(annotatedDefinition.getMetadata())) { + return true; + } + } + } + return false; + } + + private boolean isScanningDefaultPackage(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata + .getAnnotationAttributes(ComponentScan.class.getName(), true)); + if (attributes != null && hasNoScanPackageSpecified(attributes)) { + if (isInDefaultPackage(metadata.getClassName())) { + return true; + } + } + return false; + } + + private boolean hasNoScanPackageSpecified(AnnotationAttributes attributes) { + return isAllEmpty(attributes, "value", "basePackages", "basePackageClasses"); + } + + private boolean isAllEmpty(AnnotationAttributes attributes, String... names) { + for (String name : names) { + if (!ObjectUtils.isEmpty(attributes.getStringArray(name))) { + return false; + } + } + return true; + } + + protected boolean isInDefaultPackage(String className) { + String packageName = ClassUtils.getPackageName(className); + return StringUtils.isEmpty(packageName); + } + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index 8e8a6cf7ab6..a68f02672d6 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.boot.context.event.EventPublishingRunListener # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ +org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer diff --git a/spring-boot/src/test/java/org/springframework/boot/builder/SpringApplicationBuilderTests.java b/spring-boot/src/test/java/org/springframework/boot/builder/SpringApplicationBuilderTests.java index 20b6dd0275c..02200827c08 100644 --- a/spring-boot/src/test/java/org/springframework/boot/builder/SpringApplicationBuilderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/builder/SpringApplicationBuilderTests.java @@ -239,7 +239,7 @@ public class SpringApplicationBuilderTests { SpringApplicationBuilder application = new SpringApplicationBuilder( ExampleConfig.class).web(false); this.context = application.run(); - assertEquals(2, application.application().getInitializers().size()); + assertEquals(3, application.application().getInitializers().size()); } @Test @@ -247,7 +247,7 @@ public class SpringApplicationBuilderTests { SpringApplicationBuilder application = new SpringApplicationBuilder( ExampleConfig.class).child(ChildConfig.class).web(false); this.context = application.run(); - assertEquals(3, application.application().getInitializers().size()); + assertEquals(4, application.application().getInitializers().size()); } @Test @@ -261,7 +261,7 @@ public class SpringApplicationBuilderTests { } }); this.context = application.run(); - assertEquals(3, application.application().getInitializers().size()); + assertEquals(4, application.application().getInitializers().size()); } @Configuration diff --git a/spring-boot/src/test/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializerTests.java new file mode 100644 index 00000000000..2ec5ee5e53a --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/ConfigurationWarningsApplicationContextInitializerTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-2014 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.context; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer.ComponentScanDefaultPackageCheck; +import org.springframework.boot.context.configwarnings.InDefaultPackageConfiguration; +import org.springframework.boot.context.configwarnings.InDefaultPackageWithBasePackageClassesConfiguration; +import org.springframework.boot.context.configwarnings.InDefaultPackageWithBasePackagesConfiguration; +import org.springframework.boot.context.configwarnings.InDefaultPackageWithMetaAnnotationConfiguration; +import org.springframework.boot.context.configwarnings.InDefaultPackageWithValueConfiguration; +import org.springframework.boot.context.configwarnings.InDefaultPackageWithoutScanConfiguration; +import org.springframework.boot.context.configwarnings.InRealPackageConfiguration; +import org.springframework.boot.test.OutputCapture; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ConfigurationWarningsApplicationContextInitializer}. + * + * @author Phillip Webb + */ +public class ConfigurationWarningsApplicationContextInitializerTests { + + private static final String SCAN_WARNING = "Your ApplicationContext is unlikely to " + + "start due to a @ComponentScan of the default package"; + + @Rule + public OutputCapture output = new OutputCapture(); + + @Test + public void logWarningInDefaultPackage() { + load(InDefaultPackageConfiguration.class); + assertThat(this.output.toString(), containsString(SCAN_WARNING)); + } + + @Test + public void logWarningInDefaultPackageAndMetaAnnotation() { + load(InDefaultPackageWithMetaAnnotationConfiguration.class); + assertThat(this.output.toString(), containsString(SCAN_WARNING)); + } + + @Test + public void noLogIfInRealPackage() throws Exception { + load(InRealPackageConfiguration.class); + assertThat(this.output.toString(), not(containsString(SCAN_WARNING))); + } + + @Test + public void noLogWithoutComponetScanAnnotation() throws Exception { + load(InDefaultPackageWithoutScanConfiguration.class); + assertThat(this.output.toString(), not(containsString(SCAN_WARNING))); + } + + @Test + public void noLogIfHasValue() throws Exception { + load(InDefaultPackageWithValueConfiguration.class); + assertThat(this.output.toString(), not(containsString(SCAN_WARNING))); + } + + @Test + public void noLogIfHasBasePackages() throws Exception { + load(InDefaultPackageWithBasePackagesConfiguration.class); + assertThat(this.output.toString(), not(containsString(SCAN_WARNING))); + } + + @Test + public void noLogIfHasBasePackageClasses() throws Exception { + load(InDefaultPackageWithBasePackageClassesConfiguration.class); + assertThat(this.output.toString(), not(containsString(SCAN_WARNING))); + } + + private void load(Class configClass) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + new TestConfigurationWarningsApplicationContextInitializer().initialize(context); + context.register(configClass); + try { + context.refresh(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + finally { + context.close(); + } + } + + /** + * Testable version of {@link ConfigurationWarningsApplicationContextInitializer}. + */ + public static class TestConfigurationWarningsApplicationContextInitializer extends + ConfigurationWarningsApplicationContextInitializer { + + @Override + protected Check[] getChecks() { + return new Check[] { new TestComponentScanDefaultPackageCheck() }; + } + + } + + /** + * Testable ComponentScanDefaultPackageCheck that doesn't need to use the default + * package. + */ + static class TestComponentScanDefaultPackageCheck extends + ComponentScanDefaultPackageCheck { + + @Override + protected boolean isInDefaultPackage(String className) { + return className.contains("InDefault"); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageConfiguration.java new file mode 100644 index 00000000000..d7f3f213a1e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan +public class InDefaultPackageConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackageClassesConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackageClassesConfiguration.java new file mode 100644 index 00000000000..212974fd8a1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackageClassesConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.boot.context.configwarnings.nested.ExampleBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackageClasses = ExampleBean.class) +public class InDefaultPackageWithBasePackageClassesConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackagesConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackagesConfiguration.java new file mode 100644 index 00000000000..e4224b4da22 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithBasePackagesConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = "org.springframework.boot.context.configwarnings.nested") +public class InDefaultPackageWithBasePackagesConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithMetaAnnotationConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithMetaAnnotationConfiguration.java new file mode 100644 index 00000000000..6e71bbc7eb8 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithMetaAnnotationConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.Configuration; + +@Configuration +@MetaComponentScan +public class InDefaultPackageWithMetaAnnotationConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithValueConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithValueConfiguration.java new file mode 100644 index 00000000000..fcee1813fe6 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithValueConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.springframework.boot.context.configwarnings.nested") +public class InDefaultPackageWithValueConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithoutScanConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithoutScanConfiguration.java new file mode 100644 index 00000000000..8cebbc17a94 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InDefaultPackageWithoutScanConfiguration.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class InDefaultPackageWithoutScanConfiguration { +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InRealPackageConfiguration.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InRealPackageConfiguration.java new file mode 100644 index 00000000000..66a912a98ff --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/InRealPackageConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan +public class InRealPackageConfiguration { +} \ No newline at end of file diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/MetaComponentScan.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/MetaComponentScan.java new file mode 100644 index 00000000000..f48a73bdede --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/MetaComponentScan.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2014 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.context.configwarnings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.ComponentScan; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ComponentScan +public @interface MetaComponentScan { + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/nested/ExampleBean.java b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/nested/ExampleBean.java new file mode 100644 index 00000000000..abce686f2b1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/configwarnings/nested/ExampleBean.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2014 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.context.configwarnings.nested; + +import org.springframework.stereotype.Component; + +@Component +public class ExampleBean { + +}