Commit 9a537070 authored by Phillip Webb's avatar Phillip Webb

Add @ImportAutoConfiguration annotation for tests

Add a new `@ImportAutoConfiguration` annotation that can be used by
tests that wish to selectively import certain auto-configuration
classes. Also add `@AutoConfigurationPackage` so that package
registration is decoupled from `@EnableAutoConfiguration`.

An added benefit of the change is @EnableAutoConfigurationImportSelector
can now be subclassed to provide custom annotation support if needed.

Fixes gh-3660
See gh-2772
parent 8d92236e
/*
* 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 {
}
...@@ -71,8 +71,8 @@ import org.springframework.core.io.support.SpringFactoriesLoader; ...@@ -71,8 +71,8 @@ import org.springframework.core.io.support.SpringFactoriesLoader;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
@Inherited @Inherited
@Import({ EnableAutoConfigurationImportSelector.class, @AutoConfigurationPackage
AutoConfigurationPackages.Registrar.class }) @Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { public @interface EnableAutoConfiguration {
/** /**
......
...@@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure; ...@@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
...@@ -41,18 +42,21 @@ import org.springframework.core.io.ResourceLoader; ...@@ -41,18 +42,21 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/** /**
* {@link DeferredImportSelector} to handle {@link EnableAutoConfiguration * {@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 Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @see EnableAutoConfiguration * @see EnableAutoConfiguration
* @since 1.3.0
*/ */
@Order(Ordered.LOWEST_PRECEDENCE - 1) @Order(Ordered.LOWEST_PRECEDENCE - 1)
class EnableAutoConfigurationImportSelector implements DeferredImportSelector, public class EnableAutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware {
private ConfigurableListableBeanFactory beanFactory; private ConfigurableListableBeanFactory beanFactory;
...@@ -66,55 +70,113 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector, ...@@ -66,55 +70,113 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector,
@Override @Override
public String[] selectImports(AnnotationMetadata metadata) { public String[] selectImports(AnnotationMetadata metadata) {
try { try {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata AnnotationAttributes attributes = getAttributes(metadata);
.getAnnotationAttributes(EnableAutoConfiguration.class.getName(), List<String> configurations = getCandidateConfigurations(metadata, attributes);
true)); configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(metadata, attributes);
Assert.notNull(attributes, "No auto-configuration attributes found. Is " configurations.removeAll(exclusions);
+ metadata.getClassName() configurations = sort(configurations);
+ " annotated with @EnableAutoConfiguration?"); recordWithConditionEvaluationReport(configurations, exclusions);
return configurations.toArray(new String[configurations.size()]);
// Find all possible auto configuration classes, filtering duplicates
List<String> factories = new ArrayList<String>(new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
this.beanClassLoader)));
// Remove those specifically excluded
Set<String> excluded = new LinkedHashSet<String>();
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()]);
} }
catch (IOException ex) { catch (IOException ex) {
throw new IllegalStateException(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<String> 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<String> getExclusions(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<String>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
private List<String> getExcludeAutoConfigurationsProperty() { private List<String> getExcludeAutoConfigurationsProperty() {
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(getEnvironment(),
"spring.autoconfigure."); "spring.autoconfigure.");
String[] exclude = resolver.getProperty("exclude", String[].class); String[] exclude = resolver.getProperty("exclude", String[].class);
return (Arrays.asList(exclude == null ? new String[0] : exclude)); return (Arrays.asList(exclude == null ? new String[0] : exclude));
} }
@Override private List<String> sort(List<String> configurations) throws IOException {
public void setBeanClassLoader(ClassLoader classLoader) { configurations = new AutoConfigurationSorter(getResourceLoader())
this.beanClassLoader = classLoader; .getInPriorityOrder(configurations);
return configurations;
} }
@Override private void recordWithConditionEvaluationReport(List<String> configurations,
public void setResourceLoader(ResourceLoader resourceLoader) { Collection<String> exclusions) throws IOException {
this.resourceLoader = resourceLoader; ConditionEvaluationReport report = ConditionEvaluationReport
.get(getBeanFactory());
report.recordEvaluationCandidates(configurations);
report.recordExclusions(exclusions);
}
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<T>(new LinkedHashSet<T>(list));
}
protected final List<String> asList(AnnotationAttributes attributes, String name) {
String[] value = attributes.getStringArray(name);
return Arrays.asList(value == null ? new String[0] : value);
} }
@Override @Override
...@@ -123,9 +185,35 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector, ...@@ -123,9 +185,35 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector,
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; 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 @Override
public void setEnvironment(Environment environment) { public void setEnvironment(Environment environment) {
this.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;
}
} }
/*
* 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();
}
/*
* 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<String> getExclusions(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
return Collections.emptySet();
}
@Override
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
return asList(attributes, "value");
}
}
/*
* 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;
...@@ -168,4 +168,5 @@ public class EnableAutoConfigurationImportSelectorTests { ...@@ -168,4 +168,5 @@ public class EnableAutoConfigurationImportSelectorTests {
return SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, return SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
getClass().getClassLoader()); getClass().getClassLoader());
} }
} }
...@@ -18,9 +18,11 @@ package org.springframework.boot.autoconfigure.context; ...@@ -18,9 +18,11 @@ package org.springframework.boot.autoconfigure.context;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
...@@ -44,8 +46,7 @@ public class ConfigurationPropertiesAutoConfigurationTests { ...@@ -44,8 +46,7 @@ public class ConfigurationPropertiesAutoConfigurationTests {
@Test @Test
public void processAnnotatedBean() { public void processAnnotatedBean() {
load(new Class[] { SampleBean.class, load(new Class[] { AutoConfig.class, SampleBean.class }, "foo.name:test");
ConfigurationPropertiesAutoConfiguration.class }, "foo.name:test");
assertThat(this.context.getBean(SampleBean.class).getName(), is("test")); assertThat(this.context.getBean(SampleBean.class).getName(), is("test"));
} }
...@@ -62,6 +63,12 @@ public class ConfigurationPropertiesAutoConfigurationTests { ...@@ -62,6 +63,12 @@ public class ConfigurationPropertiesAutoConfigurationTests {
this.context.refresh(); this.context.refresh();
} }
@Configuration
@ImportAutoConfiguration(ConfigurationPropertiesAutoConfiguration.class)
static class AutoConfig {
}
@Component @Component
@ConfigurationProperties("foo") @ConfigurationProperties("foo")
static class SampleBean { static class SampleBean {
......
/*
* 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);
}
}
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