From 2f0f25f5adab3acb95e9c4b9c5bfd02d8aac7588 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 19 Jul 2017 08:51:21 -0700 Subject: [PATCH] Add Configurations class Add a general purpose `Configurations` class that encapsulates the sorting and merging rules that are usually apply. The class is particularly useful in tests where configuration classes often need to be specified, but an `@Import` or `ImportSelector` cannot be easily used. Two `Configurations` subclasses have been initially added. The `UserConfigurations` class can be used to represent user defined configuration and the `AutoConfigurations` class can be used to represent a subset of auto-configurations. Auto configurations are sorted using the same `@AutoConfiguraionBefore`/`@AutoConfiguraionAfter` logic as the `@EnableAutoConfiguration` annotation. Fixes gh-9795 --- .../AutoConfigurationSorter.java | 11 +- .../autoconfigure/AutoConfigurations.java | 72 +++++++++ .../AutoConfigurationsTests.java | 49 ++++++ .../context/annotation/Configurations.java | 139 ++++++++++++++++++ .../annotation/UserConfigurations.java | 53 +++++++ .../annotation/ConfigurationsTests.java | 111 ++++++++++++++ .../annotation/UserConfigurationsTests.java | 41 ++++++ 7 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/annotation/UserConfigurations.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/annotation/UserConfigurationsTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index 2842a80419..d30bbabb42 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -168,7 +168,7 @@ class AutoConfigurationSorter { } private int getOrder() { - if (this.autoConfigurationMetadata.wasProcessed(this.className)) { + if (wasProcessed()) { return this.autoConfigurationMetadata.getInteger(this.className, "AutoConfigureOrder", Ordered.LOWEST_PRECEDENCE); } @@ -179,7 +179,7 @@ class AutoConfigurationSorter { } private Set readBefore() { - if (this.autoConfigurationMetadata.wasProcessed(this.className)) { + if (wasProcessed()) { return this.autoConfigurationMetadata.getSet(this.className, "AutoConfigureBefore", Collections.emptySet()); } @@ -187,13 +187,18 @@ class AutoConfigurationSorter { } private Set readAfter() { - if (this.autoConfigurationMetadata.wasProcessed(this.className)) { + if (wasProcessed()) { return this.autoConfigurationMetadata.getSet(this.className, "AutoConfigureAfter", Collections.emptySet()); } return getAnnotationValue(AutoConfigureAfter.class); } + private boolean wasProcessed() { + return (this.autoConfigurationMetadata != null + && this.autoConfigurationMetadata.wasProcessed(this.className)); + } + private Set getAnnotationValue(Class annotation) { Map attributes = getAnnotationMetadata() .getAnnotationAttributes(annotation.getName(), true); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java new file mode 100644 index 0000000000..0b2dd29375 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2017 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.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.context.annotation.Configurations; +import org.springframework.core.Ordered; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.ClassUtils; + +/** + * {@link Configurations} representing auto-configuration {@code @Configuration} classes. + * + * @author Philip Webb + * @since 2.0.0 + */ +public class AutoConfigurations extends Configurations implements Ordered { + + private static AutoConfigurationSorter SORTER = new AutoConfigurationSorter( + new SimpleMetadataReaderFactory(), null); + + private static final Ordered ORDER = new AutoConfigurationImportSelector(); + + protected AutoConfigurations(Collection> classes) { + super(classes); + } + + @Override + protected Collection> sort(Collection> classes) { + List names = classes.stream().map(Class::getName) + .collect(Collectors.toCollection(ArrayList::new)); + List sorted = SORTER.getInPriorityOrder(names); + return sorted.stream() + .map(className -> ClassUtils.resolveClassName(className, null)) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public int getOrder() { + return ORDER.getOrder(); + } + + @Override + protected AutoConfigurations merge(Set> mergedClasses) { + return new AutoConfigurations(mergedClasses); + } + + public static AutoConfigurations of(Class... classes) { + return new AutoConfigurations(Arrays.asList(classes)); + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java new file mode 100644 index 0000000000..2dd4ba05f5 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2017 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 org.junit.Test; + +import org.springframework.boot.context.annotation.Configurations; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigurations}. + * + * @author Phillip Webb + */ +public class AutoConfigurationsTests { + + @Test + public void ofShouldCreateOrderedConfigurations() throws Exception { + Configurations configurations = AutoConfigurations.of(AutoConfigureA.class, + AutoConfigureB.class); + assertThat(Configurations.getClasses(configurations)) + .containsExactly(AutoConfigureB.class, AutoConfigureA.class); + } + + @AutoConfigureAfter(AutoConfigureB.class) + public static class AutoConfigureA { + + } + + public static class AutoConfigureB { + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java b/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java new file mode 100644 index 0000000000..62d37e182a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2017 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.context.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.Assert; + +/** + * A set of {@link Configuration @Configuration} classes that can be registered + * {@link ApplicationContext}. Classes can be returned from one or more + * {@link Configurations} instance by using {@link #getClasses(Configurations[])}. The + * resulting array follows the ordering rules usually applied by the + * {@link ApplicationContext} and/or custom {@link ImportSelector} implementations. + *

+ * This class is primarily intended for use with tests that need to specify configuration + * classes but can't use {@link SpringRunner}. + *

+ * Implementations of this class should be annotated with {@code @Order} or implement + * {@link Ordered}. + * + * @author Phillip Webb + * @since 2.0.0 + * @see UserConfigurations + */ +public abstract class Configurations { + + private static final Comparator COMPARATOR = OrderComparator.INSTANCE + .thenComparing(o -> o.getClass().getName()); + + private Set> classes; + + protected Configurations(Collection> classes) { + Assert.notNull(classes, "Classes must not be null"); + Collection> sorted = sort(classes); + this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted)); + } + + /** + * Sort configuration classes into the order that they should be applied. + * @param classes the classes to sort + * @return a sorted set of classes + */ + protected Collection> sort(Collection> classes) { + return classes; + } + + protected final Set> getClasses() { + return this.classes; + } + + /** + * Merge configurations from another source of the same type. + * @param other the other {@link Configurations} (must be of the same type as this + * instance) + * @return a new configurations instance (must be of the same type as this instance) + */ + protected Configurations merge(Configurations other) { + Set> mergedClasses = new LinkedHashSet<>(getClasses()); + mergedClasses.addAll(other.getClasses()); + return merge(mergedClasses); + } + + /** + * Merge configurations. + * @param mergedClasses the merge classes + * @return a new configurations instance (must be of the same type as this instance) + */ + protected abstract Configurations merge(Set> mergedClasses); + + /** + * Return the classes from all the specified configurations in the oder that they + * would be registered. + * @param configurations the source configuration + * @return configuration classes in registration order + */ + public static Class[] getClasses(Configurations... configurations) { + return getClasses(Arrays.asList(configurations)); + } + + /** + * Return the classes from all the specified configurations in the oder that they + * would be registered. + * @param configurations the source configuration + * @return configuration classes in registration order + */ + public static Class[] getClasses(Collection configurations) { + List orderedConfigurations = new ArrayList<>(configurations); + orderedConfigurations.sort(COMPARATOR); + List collated = collate(orderedConfigurations); + return collated.stream().flatMap(c -> c.getClasses().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)) + .toArray(new Class[0]); + } + + private static List collate( + List orderedConfigurations) { + LinkedList collated = new LinkedList<>(); + for (Configurations item : orderedConfigurations) { + if (collated.isEmpty() || collated.getLast().getClass() != item.getClass()) { + collated.add(item); + } + else { + collated.set(collated.size() - 1, collated.getLast().merge(item)); + } + } + return collated; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/annotation/UserConfigurations.java b/spring-boot/src/main/java/org/springframework/boot/context/annotation/UserConfigurations.java new file mode 100644 index 0000000000..dbd1cce059 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/annotation/UserConfigurations.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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.context.annotation; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +/** + * {@link Configurations} representing user-defined {@code @Configuration} classes (i.e. + * those defined in classes usually written by the user). + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class UserConfigurations extends Configurations implements PriorityOrdered { + + protected UserConfigurations(Collection> classes) { + super(classes); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + protected UserConfigurations merge(Set> mergedClasses) { + return new UserConfigurations(mergedClasses); + } + + public static UserConfigurations of(Class... classes) { + return new UserConfigurations(Arrays.asList(classes)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java b/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java new file mode 100644 index 0000000000..9ec6b7b9fa --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2017 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.context.annotation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; + +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.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Configurations}. + * + * @author Phillip Webb + */ +public class ConfigurationsTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenClassesIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Classes must not be null"); + new TestConfigurations(null); + } + + @Test + public void createShouldSortClasses() throws Exception { + TestSortedConfigurations configurations = new TestSortedConfigurations( + Arrays.asList(OutputStream.class, InputStream.class)); + assertThat(configurations.getClasses()).containsExactly(InputStream.class, + OutputStream.class); + } + + @Test + public void getClassesShouldMergeByClassAndSort() throws Exception { + Configurations c1 = new TestSortedConfigurations( + Arrays.asList(OutputStream.class, InputStream.class)); + Configurations c2 = new TestConfigurations(Arrays.asList(Short.class)); + Configurations c3 = new TestSortedConfigurations( + Arrays.asList(String.class, Integer.class)); + Configurations c4 = new TestConfigurations(Arrays.asList(Long.class, Byte.class)); + Class[] classes = Configurations.getClasses(c1, c2, c3, c4); + System.out.println(Arrays.asList(classes)); + assertThat(classes).containsExactly(Short.class, Long.class, Byte.class, + InputStream.class, Integer.class, OutputStream.class, String.class); + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + static class TestConfigurations extends Configurations { + + protected TestConfigurations(Collection> classes) { + super(classes); + } + + @Override + protected Configurations merge(Set> mergedClasses) { + return new TestConfigurations(mergedClasses); + } + + } + + @Order(Ordered.LOWEST_PRECEDENCE) + static class TestSortedConfigurations extends Configurations { + + protected TestSortedConfigurations(Collection> classes) { + super(classes); + } + + @Override + protected Collection> sort(Collection> classes) { + ArrayList> sorted = new ArrayList<>(classes); + sorted.sort(Comparator.comparing(ClassUtils::getShortName)); + return sorted; + } + + @Override + protected Configurations merge(Set> mergedClasses) { + return new TestSortedConfigurations(mergedClasses); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/context/annotation/UserConfigurationsTests.java b/spring-boot/src/test/java/org/springframework/boot/context/annotation/UserConfigurationsTests.java new file mode 100644 index 0000000000..872848c801 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/annotation/UserConfigurationsTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2017 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.context.annotation; + +import java.io.InputStream; +import java.io.OutputStream; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UserConfigurations}. + * + * @author Phillip Webb + */ +public class UserConfigurationsTests { + + @Test + public void ofShouldCreateUnorderedConfigurations() { + UserConfigurations configurations = UserConfigurations.of(OutputStream.class, + InputStream.class); + assertThat(Configurations.getClasses(configurations)) + .containsExactly(OutputStream.class, InputStream.class); + } + +}