From b572d98cbf2d041ce7fcab212296a952620a6041 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Jun 2013 12:26:45 -0700 Subject: [PATCH] Support ordering of auto-configuration classes Update EnableAutoConfigurationImportSelector to sort auto-configuration classes based on @Order and @AutoConfigureAfter annotations. --- .../annotation/AutoConfigurationSorter.java | 172 ++++++++++++++++++ .../annotation/AutoConfigureAfter.java | 33 ++++ .../annotation/EnableAutoConfiguration.java | 1 + ...EnableAutoConfigurationImportSelector.java | 43 ++++- .../AutoConfigurationSorterTest.java | 104 +++++++++++ 5 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorter.java create mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigureAfter.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorterTest.java diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorter.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorter.java new file mode 100644 index 0000000000..f3f2a71b0d --- /dev/null +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorter.java @@ -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 getInPriorityOrder(Collection classNames) + throws IOException { + List autoConfigurationClasses = new ArrayList(); + 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 orderedClassNames = new ArrayList(); + for (AutoConfigurationClass autoConfigurationClass : autoConfigurationClasses) { + orderedClassNames.add(autoConfigurationClass.toString()); + } + return orderedClassNames; + } + + private List sortByAfterAnnotation( + Collection autoConfigurationClasses) + throws IOException { + List tosort = new ArrayList( + autoConfigurationClasses); + Set sorted = new LinkedHashSet(); + Set processing = new LinkedHashSet(); + while (!tosort.isEmpty()) { + doSortByAfterAnnotation(tosort, sorted, processing, null); + } + return new ArrayList(sorted); + } + + private void doSortByAfterAnnotation(List tosort, + Set sorted, Set 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 after; + + private Map 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 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 getAfter() throws IOException { + if (this.after == null) { + if (this.afterAnnotation == null) { + this.after = Collections.emptyList(); + } else { + this.after = new ArrayList(); + 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); + } + } + +} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigureAfter.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigureAfter.java new file mode 100644 index 0000000000..c5a0d8fee3 --- /dev/null +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AutoConfigureAfter.java @@ -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(); +} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfiguration.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfiguration.java index e6b4d007f6..47bce12371 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfiguration.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfiguration.java @@ -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) diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java index df1e3b4bb3..63a975cac5 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/EnableAutoConfigurationImportSelector.java @@ -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 factories = new ArrayList( - 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 factories = new ArrayList( + 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; + } + } diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorterTest.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorterTest.java new file mode 100644 index 0000000000..d3d2110ec4 --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/AutoConfigurationSorterTest.java @@ -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 actual = this.sorter.getInPriorityOrder(Arrays.asList(LOWEST, + HIGHEST)); + assertThat(actual, equalTo(Arrays.asList(HIGHEST, LOWEST))); + } + + @Test + public void byAutoConfigureAfter() throws Exception { + List actual = this.sorter.getInPriorityOrder(Arrays.asList(A, B, C)); + assertThat(actual, equalTo(Arrays.asList(C, B, A))); + } + + @Test + public void byAutoConfigureAfterWithMissing() throws Exception { + List 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 { + } +}