Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support ordering of auto-configuration classes
Update EnableAutoConfigurationImportSelector to sort auto-configuration
classes based on @order and @AutoConfigureAfter annotations.
  • Loading branch information
philwebb committed Jun 12, 2013
commit b572d98cbf2d041ce7fcab212296a952620a6041
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* @see ConditionalOnBean
* @see ConditionalOnMissingBean
* @see ConditionalOnClass
* @see AutoConfigureAfter
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,24 +40,48 @@
*/
@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
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

}
Original file line number Diff line number Diff line change
@@ -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 {
}
}