diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java new file mode 100644 index 0000000000..e0f0bfed5d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2015 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Indicates that the package containing the annotated class should be registered with + * {@link AutoConfigurationPackages}. + * + * @author Phillip Webb + * @since 1.3.0 + * @see AutoConfigurationPackages + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(AutoConfigurationPackages.Registrar.class) +public @interface AutoConfigurationPackage { + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java index dca59dca18..aff3fcc3b2 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java @@ -71,8 +71,8 @@ import org.springframework.core.io.support.SpringFactoriesLoader; @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Import({ EnableAutoConfigurationImportSelector.class, - AutoConfigurationPackages.Registrar.class }) +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { /** diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java index bcb27ca79d..8e3ca18ad5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelector.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -41,18 +42,21 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link DeferredImportSelector} to handle {@link EnableAutoConfiguration - * auto-configuration}. + * auto-configuration}. This class can also be subclassed if a custom variant of + * {@link EnableAutoConfiguration @EnableAutoConfiguration}. is needed. * * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll * @see EnableAutoConfiguration + * @since 1.3.0 */ @Order(Ordered.LOWEST_PRECEDENCE - 1) -class EnableAutoConfigurationImportSelector implements DeferredImportSelector, +public class EnableAutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { private ConfigurableListableBeanFactory beanFactory; @@ -66,55 +70,113 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector, @Override public String[] selectImports(AnnotationMetadata metadata) { try { - AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata - .getAnnotationAttributes(EnableAutoConfiguration.class.getName(), - true)); - - Assert.notNull(attributes, "No auto-configuration attributes found. Is " - + metadata.getClassName() - + " annotated with @EnableAutoConfiguration?"); - - // Find all possible auto configuration classes, filtering duplicates - List factories = new ArrayList(new LinkedHashSet( - SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, - this.beanClassLoader))); - - // Remove those specifically excluded - Set excluded = new LinkedHashSet(); - excluded.addAll(Arrays.asList(attributes.getStringArray("exclude"))); - excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName"))); - excluded.addAll(getExcludeAutoConfigurationsProperty()); - factories.removeAll(excluded); - ConditionEvaluationReport.get(this.beanFactory).recordExclusions(excluded); - ConditionEvaluationReport.get(this.beanFactory).recordEvaluationCandidates( - factories); - - // Sort - factories = new AutoConfigurationSorter(this.resourceLoader) - .getInPriorityOrder(factories); - - return factories.toArray(new String[factories.size()]); + AnnotationAttributes attributes = getAttributes(metadata); + List configurations = getCandidateConfigurations(metadata, attributes); + configurations = removeDuplicates(configurations); + Set exclusions = getExclusions(metadata, attributes); + configurations.removeAll(exclusions); + configurations = sort(configurations); + recordWithConditionEvaluationReport(configurations, exclusions); + return configurations.toArray(new String[configurations.size()]); } catch (IOException ex) { throw new IllegalStateException(ex); } } + /** + * Return the appropriate {@link AnnotationAttributes} from the + * {@link AnnotationMetadata}. By default this method will return attributes for + * {@link #getAnnotationClass()}. + * @param metadata the annotation metadata + * @return annotation attributes + */ + protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) { + String name = getAnnotationClass().getName(); + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata + .getAnnotationAttributes(name, true)); + Assert.notNull(attributes, + "No auto-configuration attributes found. Is " + metadata.getClassName() + + " annotated with " + ClassUtils.getShortName(name) + "?"); + return attributes; + } + + /** + * Return the source annotation class used by the selector. + * @return the annotation class + */ + protected Class getAnnotationClass() { + return EnableAutoConfiguration.class; + } + + /** + * Return the auto-configuration class names that should be considered. By default + * this method will load candidates using {@link SpringFactoriesLoader} with + * {@link #getSpringFactoriesLoaderFactoryClass()}. + * @param metadata the source metadata + * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation + * attributes} + * @return a list of candidate configurations + */ + protected List getCandidateConfigurations(AnnotationMetadata metadata, + AnnotationAttributes attributes) { + return SpringFactoriesLoader.loadFactoryNames( + getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); + } + + /** + * Return the class used by {@link SpringFactoriesLoader} to load configuration + * candidates. + * @return the factory class + */ + protected Class getSpringFactoriesLoaderFactoryClass() { + return EnableAutoConfiguration.class; + } + + /** + * Return any exclusions that limit the candidate configurations. + * @param metadata the source metadata + * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation + * attributes} + * @return exclusions or an empty set + */ + protected Set getExclusions(AnnotationMetadata metadata, + AnnotationAttributes attributes) { + Set excluded = new LinkedHashSet(); + excluded.addAll(asList(attributes, "exclude")); + excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName"))); + excluded.addAll(getExcludeAutoConfigurationsProperty()); + return excluded; + } + private List getExcludeAutoConfigurationsProperty() { - RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(getEnvironment(), "spring.autoconfigure."); String[] exclude = resolver.getProperty("exclude", String[].class); return (Arrays.asList(exclude == null ? new String[0] : exclude)); } - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.beanClassLoader = classLoader; + private List sort(List configurations) throws IOException { + configurations = new AutoConfigurationSorter(getResourceLoader()) + .getInPriorityOrder(configurations); + return configurations; } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; + private void recordWithConditionEvaluationReport(List configurations, + Collection exclusions) throws IOException { + ConditionEvaluationReport report = ConditionEvaluationReport + .get(getBeanFactory()); + report.recordEvaluationCandidates(configurations); + report.recordExclusions(exclusions); + } + + protected final List removeDuplicates(List list) { + return new ArrayList(new LinkedHashSet(list)); + } + + protected final List asList(AnnotationAttributes attributes, String name) { + String[] value = attributes.getStringArray(name); + return Arrays.asList(value == null ? new String[0] : value); } @Override @@ -123,9 +185,35 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector, this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } + protected final ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + protected ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + @Override public void setEnvironment(Environment environment) { this.environment = environment; } + protected final Environment getEnvironment() { + return this.environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + protected final ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfiguration.java new file mode 100644 index 0000000000..53a3413726 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2015 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.test; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.context.annotation.Import; + +/** + * Import and apply the selected auto-configuration classes for testing purposes. Applies + * the same ordering rules as {@code @EnableAutoConfiguration} but restricts the + * auto-configuration classes to the specified set, rather than consulting + * {@code spring.factories}. + * + * @author Phillip Webb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(ImportAutoConfigurationImportSelector.class) +public @interface ImportAutoConfiguration { + + /** + * The auto-configuration classes that should be imported. + * @return the classes to import + */ + Class[] value(); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelector.java new file mode 100644 index 0000000000..3e0fd6ca39 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelector.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2015 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.test; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.autoconfigure.EnableAutoConfigurationImportSelector; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Variant of {@link EnableAutoConfigurationImportSelector} for + * {@link ImportAutoConfiguration}. + * + * @author Phillip Webb + */ +class ImportAutoConfigurationImportSelector extends EnableAutoConfigurationImportSelector { + + @Override + protected Class getAnnotationClass() { + return ImportAutoConfiguration.class; + } + + @Override + protected Set getExclusions(AnnotationMetadata metadata, + AnnotationAttributes attributes) { + return Collections.emptySet(); + } + + @Override + protected List getCandidateConfigurations(AnnotationMetadata metadata, + AnnotationAttributes attributes) { + return asList(attributes, "value"); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/package-info.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/package-info.java new file mode 100644 index 0000000000..eff42c813d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/test/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2015 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. + */ + +/** + * Test utilities related to auto-configuration. + */ +package org.springframework.boot.autoconfigure.test; + diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java index e24edb5634..3a932045ae 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/EnableAutoConfigurationImportSelectorTests.java @@ -168,4 +168,5 @@ public class EnableAutoConfigurationImportSelectorTests { return SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, getClass().getClassLoader()); } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java index 9c3c24d1ce..7329e83da7 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java @@ -18,9 +18,11 @@ package org.springframework.boot.autoconfigure.context; import org.junit.After; import org.junit.Test; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import static org.hamcrest.core.Is.is; @@ -44,8 +46,7 @@ public class ConfigurationPropertiesAutoConfigurationTests { @Test public void processAnnotatedBean() { - load(new Class[] { SampleBean.class, - ConfigurationPropertiesAutoConfiguration.class }, "foo.name:test"); + load(new Class[] { AutoConfig.class, SampleBean.class }, "foo.name:test"); assertThat(this.context.getBean(SampleBean.class).getName(), is("test")); } @@ -62,6 +63,12 @@ public class ConfigurationPropertiesAutoConfigurationTests { this.context.refresh(); } + @Configuration + @ImportAutoConfiguration(ConfigurationPropertiesAutoConfiguration.class) + static class AutoConfig { + + } + @Component @ConfigurationProperties("foo") static class SampleBean { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelectorTests.java new file mode 100644 index 0000000000..80eff536b6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/test/ImportAutoConfigurationImportSelectorTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2015 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.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.AnnotationMetadata; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link ImportAutoConfigurationImportSelector}. + * + * @author Phillip Webb + */ +@RunWith(MockitoJUnitRunner.class) +public class ImportAutoConfigurationImportSelectorTests { + + private final ImportAutoConfigurationImportSelector importSelector = new ImportAutoConfigurationImportSelector(); + + private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Mock + private Environment environment; + + @Mock + private AnnotationMetadata annotationMetadata; + + @Mock + private AnnotationAttributes annotationAttributes; + + @Before + public void configureImportSelector() { + this.importSelector.setBeanFactory(this.beanFactory); + this.importSelector.setEnvironment(this.environment); + this.importSelector.setResourceLoader(new DefaultResourceLoader()); + } + + @Test + public void importsAreSelected() { + String[] value = new String[] { FreeMarkerAutoConfiguration.class.getName() }; + configureValue(value); + String[] imports = this.importSelector.selectImports(this.annotationMetadata); + assertThat(imports, equalTo(value)); + } + + @Test + public void propertyExclusionsAreNotApplied() { + configureValue(new String[] { FreeMarkerAutoConfiguration.class.getName() }); + this.importSelector.selectImports(this.annotationMetadata); + verifyZeroInteractions(this.environment); + } + + private void configureValue(String... value) { + String name = ImportAutoConfiguration.class.getName(); + given(this.annotationMetadata.getAnnotationAttributes(name, true)).willReturn( + this.annotationAttributes); + given(this.annotationAttributes.getStringArray("value")).willReturn(value); + } + +}