Support ordering of auto-configuration classes

Update EnableAutoConfigurationImportSelector to sort auto-configuration
classes based on @Order and @AutoConfigureAfter annotations.
This commit is contained in:
Phillip Webb 2013-06-12 12:26:45 -07:00
parent 3536fc68f5
commit b572d98cbf
5 changed files with 345 additions and 8 deletions

View File

@ -0,0 +1,172 @@
/*
* 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.bootstrap.context.annotation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.util.Assert;
/**
* Sort {@link EnableAutoConfiguration auto-configuration} classes into priority order by
* reading {@link Ordered} and {@link AutoConfigureAfter} annotations (without loading
* classes).
*
* @author Phillip Webb
*/
class AutoConfigurationSorter {
private CachingMetadataReaderFactory metadataReaderFactory;
public AutoConfigurationSorter(ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
}
public List<String> getInPriorityOrder(Collection<String> classNames)
throws IOException {
List<AutoConfigurationClass> autoConfigurationClasses = new ArrayList<AutoConfigurationClass>();
for (String className : classNames) {
autoConfigurationClasses.add(new AutoConfigurationClass(className));
}
// Sort initially by order
Collections.sort(autoConfigurationClasses, OrderComparator.INSTANCE);
// Then respect @AutoConfigureAfter
autoConfigurationClasses = sortByAfterAnnotation(autoConfigurationClasses);
List<String> orderedClassNames = new ArrayList<String>();
for (AutoConfigurationClass autoConfigurationClass : autoConfigurationClasses) {
orderedClassNames.add(autoConfigurationClass.toString());
}
return orderedClassNames;
}
private List<AutoConfigurationClass> sortByAfterAnnotation(
Collection<AutoConfigurationClass> autoConfigurationClasses)
throws IOException {
List<AutoConfigurationClass> tosort = new ArrayList<AutoConfigurationClass>(
autoConfigurationClasses);
Set<AutoConfigurationClass> sorted = new LinkedHashSet<AutoConfigurationClass>();
Set<AutoConfigurationClass> processing = new LinkedHashSet<AutoConfigurationClass>();
while (!tosort.isEmpty()) {
doSortByAfterAnnotation(tosort, sorted, processing, null);
}
return new ArrayList<AutoConfigurationClass>(sorted);
}
private void doSortByAfterAnnotation(List<AutoConfigurationClass> tosort,
Set<AutoConfigurationClass> sorted, Set<AutoConfigurationClass> processing,
AutoConfigurationClass current) throws IOException {
if (current == null) {
current = tosort.remove(0);
}
processing.add(current);
for (AutoConfigurationClass after : current.getAfter()) {
Assert.state(!processing.contains(after),
"Cycle @AutoConfigureAfter detected between " + current + " and "
+ after);
if (!sorted.contains(after) && tosort.contains(after)) {
doSortByAfterAnnotation(tosort, sorted, processing, after);
}
}
processing.remove(current);
sorted.add(current);
}
private class AutoConfigurationClass implements Ordered {
private final String className;
private final int order;
private List<AutoConfigurationClass> after;
private Map<String, Object> afterAnnotation;
public AutoConfigurationClass(String className) throws IOException {
this.className = className;
MetadataReader metadataReader = AutoConfigurationSorter.this.metadataReaderFactory
.getMetadataReader(className);
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
// Read @Order annotation
Map<String, Object> orderedAnnotation = metadata
.getAnnotationAttributes(Order.class.getName());
this.order = (orderedAnnotation == null ? Ordered.LOWEST_PRECEDENCE
: (Integer) orderedAnnotation.get("value"));
// Read @AutoConfigureAfter annotation
this.afterAnnotation = metadata.getAnnotationAttributes(
AutoConfigureAfter.class.getName(), true);
}
@Override
public int getOrder() {
return this.order;
}
public List<AutoConfigurationClass> getAfter() throws IOException {
if (this.after == null) {
if (this.afterAnnotation == null) {
this.after = Collections.emptyList();
} else {
this.after = new ArrayList<AutoConfigurationClass>();
for (String afterClass : (String[]) this.afterAnnotation.get("value")) {
this.after.add(new AutoConfigurationClass(afterClass));
}
}
}
return this.after;
}
@Override
public String toString() {
return this.className;
}
@Override
public int hashCode() {
return this.className.hashCode();
}
@Override
public boolean equals(Object obj) {
return this.className.equals(((AutoConfigurationClass) obj).className);
}
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.bootstrap.context.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Hint for that an {@link EnableAutoConfiguration auto-configuration} should be applied
* after the specified auto-configuration classes.
*
* @author Phillip Webb
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface AutoConfigureAfter {
Class<?>[] value();
}

View File

@ -55,6 +55,7 @@ import org.springframework.core.io.support.SpringFactoriesLoader;
* @see ConditionalOnBean
* @see ConditionalOnMissingBean
* @see ConditionalOnClass
* @see AutoConfigureAfter
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

View File

@ -16,15 +16,18 @@
package org.springframework.bootstrap.context.annotation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata;
@ -37,19 +40,38 @@ import org.springframework.core.type.AnnotationMetadata;
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class EnableAutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware {
BeanClassLoaderAware, ResourceLoaderAware {
private ClassLoader beanClassLoader;
private ResourceLoader resourceLoader;
@Override
public String[] selectImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata
.getAnnotationAttributes(EnableAutoConfiguration.class.getName(), true));
List<String> factories = new ArrayList<String>(
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
this.beanClassLoader));
factories.removeAll(Arrays.asList(attributes.getStringArray("exclude")));
return factories.toArray(new String[factories.size()]);
try {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata
.getAnnotationAttributes(EnableAutoConfiguration.class.getName(),
true));
// Find all possible auto configuration classes
List<String> factories = new ArrayList<String>(
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
this.beanClassLoader));
// Remove those specifically disabled
factories.removeAll(Arrays.asList(attributes.getStringArray("exclude")));
// Sort
factories = new AutoConfigurationSorter(this.resourceLoader)
.getInPriorityOrder(factories);
// Always add the ComponentScanDetector as the first in the list
factories.add(0, ComponentScanDetector.class.getName());
return factories.toArray(new String[factories.size()]);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
@Override
@ -57,4 +79,9 @@ class EnableAutoConfigurationImportSelector implements DeferredImportSelector,
this.beanClassLoader = classLoader;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.bootstrap.context.annotation;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.DefaultResourceLoader;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link AutoConfigurationSorter}.
*
* @author Phillip Webb
*/
public class AutoConfigurationSorterTest {
private static final String LOWEST = OrderLowest.class.getName();
private static final String HIGHEST = OrderHighest.class.getName();
private static final String A = AutoConfigureA.class.getName();
private static final String B = AutoConfigureB.class.getName();
private static final String C = AutoConfigureC.class.getName();
private static final String D = AutoConfigureD.class.getName();
@Rule
public ExpectedException thrown = ExpectedException.none();
private AutoConfigurationSorter sorter;
@Before
public void setup() {
this.sorter = new AutoConfigurationSorter(new DefaultResourceLoader());
}
@Test
public void byOrderAnnotation() throws Exception {
List<String> actual = this.sorter.getInPriorityOrder(Arrays.asList(LOWEST,
HIGHEST));
assertThat(actual, equalTo(Arrays.asList(HIGHEST, LOWEST)));
}
@Test
public void byAutoConfigureAfter() throws Exception {
List<String> actual = this.sorter.getInPriorityOrder(Arrays.asList(A, B, C));
assertThat(actual, equalTo(Arrays.asList(C, B, A)));
}
@Test
public void byAutoConfigureAfterWithMissing() throws Exception {
List<String> actual = this.sorter.getInPriorityOrder(Arrays.asList(A, B));
assertThat(actual, equalTo(Arrays.asList(B, A)));
}
@Test
public void byAutoConfigureAfterWithCycle() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Cycle");
this.sorter.getInPriorityOrder(Arrays.asList(A, B, C, D));
}
@Order(Ordered.LOWEST_PRECEDENCE)
public static class OrderLowest {
}
@Order(Ordered.HIGHEST_PRECEDENCE)
public static class OrderHighest {
}
@AutoConfigureAfter(AutoConfigureB.class)
public static class AutoConfigureA {
}
@AutoConfigureAfter({ AutoConfigureC.class, AutoConfigureD.class })
public static class AutoConfigureB {
}
public static class AutoConfigureC {
}
@AutoConfigureAfter(AutoConfigureA.class)
public static class AutoConfigureD {
}
}