Commit 1a573519 authored by Phillip Webb's avatar Phillip Webb

Warn on @ComponentScan of default package

Add ConfigurationWarningsApplicationContextInitializer to report
warnings for common configuration mistakes. Currently the initializer
will log a warning if @ComponentScan is used on a @Configuration class
in the "default" package.

Fixes gh-2050
parent 447c9ff2
/*
* 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<ConfigurableApplicationContext> {
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);
}
}
}
...@@ -9,6 +9,7 @@ org.springframework.boot.context.event.EventPublishingRunListener ...@@ -9,6 +9,7 @@ org.springframework.boot.context.event.EventPublishingRunListener
# Application Context Initializers # Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\ org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer org.springframework.boot.context.config.DelegatingApplicationContextInitializer
......
...@@ -239,7 +239,7 @@ public class SpringApplicationBuilderTests { ...@@ -239,7 +239,7 @@ public class SpringApplicationBuilderTests {
SpringApplicationBuilder application = new SpringApplicationBuilder( SpringApplicationBuilder application = new SpringApplicationBuilder(
ExampleConfig.class).web(false); ExampleConfig.class).web(false);
this.context = application.run(); this.context = application.run();
assertEquals(2, application.application().getInitializers().size()); assertEquals(3, application.application().getInitializers().size());
} }
@Test @Test
...@@ -247,7 +247,7 @@ public class SpringApplicationBuilderTests { ...@@ -247,7 +247,7 @@ public class SpringApplicationBuilderTests {
SpringApplicationBuilder application = new SpringApplicationBuilder( SpringApplicationBuilder application = new SpringApplicationBuilder(
ExampleConfig.class).child(ChildConfig.class).web(false); ExampleConfig.class).child(ChildConfig.class).web(false);
this.context = application.run(); this.context = application.run();
assertEquals(3, application.application().getInitializers().size()); assertEquals(4, application.application().getInitializers().size());
} }
@Test @Test
...@@ -261,7 +261,7 @@ public class SpringApplicationBuilderTests { ...@@ -261,7 +261,7 @@ public class SpringApplicationBuilderTests {
} }
}); });
this.context = application.run(); this.context = application.run();
assertEquals(3, application.application().getInitializers().size()); assertEquals(4, application.application().getInitializers().size());
} }
@Configuration @Configuration
......
/*
* 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");
}
}
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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
/*
* 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 {
}
/*
* 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 {
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment