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
pull/9800/merge
Phillip Webb 7 years ago
parent 9f6d8c6778
commit 2f0f25f5ad

@ -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<String> readBefore() {
if (this.autoConfigurationMetadata.wasProcessed(this.className)) {
if (wasProcessed()) {
return this.autoConfigurationMetadata.getSet(this.className,
"AutoConfigureBefore", Collections.<String>emptySet());
}
@ -187,13 +187,18 @@ class AutoConfigurationSorter {
}
private Set<String> readAfter() {
if (this.autoConfigurationMetadata.wasProcessed(this.className)) {
if (wasProcessed()) {
return this.autoConfigurationMetadata.getSet(this.className,
"AutoConfigureAfter", Collections.<String>emptySet());
}
return getAnnotationValue(AutoConfigureAfter.class);
}
private boolean wasProcessed() {
return (this.autoConfigurationMetadata != null
&& this.autoConfigurationMetadata.wasProcessed(this.className));
}
private Set<String> getAnnotationValue(Class<?> annotation) {
Map<String, Object> attributes = getAnnotationMetadata()
.getAnnotationAttributes(annotation.getName(), true);

@ -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<Class<?>> classes) {
super(classes);
}
@Override
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
List<String> names = classes.stream().map(Class::getName)
.collect(Collectors.toCollection(ArrayList::new));
List<String> 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<Class<?>> mergedClasses) {
return new AutoConfigurations(mergedClasses);
}
public static AutoConfigurations of(Class<?>... classes) {
return new AutoConfigurations(Arrays.asList(classes));
}
}

@ -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 {
}
}

@ -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.
* <p>
* This class is primarily intended for use with tests that need to specify configuration
* classes but can't use {@link SpringRunner}.
* <p>
* 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<Object> COMPARATOR = OrderComparator.INSTANCE
.thenComparing(o -> o.getClass().getName());
private Set<Class<?>> classes;
protected Configurations(Collection<Class<?>> classes) {
Assert.notNull(classes, "Classes must not be null");
Collection<Class<?>> 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<Class<?>> sort(Collection<Class<?>> classes) {
return classes;
}
protected final Set<Class<?>> 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<Class<?>> 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<Class<?>> 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> configurations) {
List<Configurations> orderedConfigurations = new ArrayList<>(configurations);
orderedConfigurations.sort(COMPARATOR);
List<Configurations> collated = collate(orderedConfigurations);
return collated.stream().flatMap(c -> c.getClasses().stream())
.collect(Collectors.toCollection(LinkedHashSet::new))
.toArray(new Class<?>[0]);
}
private static List<Configurations> collate(
List<Configurations> orderedConfigurations) {
LinkedList<Configurations> 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;
}
}

@ -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<Class<?>> classes) {
super(classes);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
protected UserConfigurations merge(Set<Class<?>> mergedClasses) {
return new UserConfigurations(mergedClasses);
}
public static UserConfigurations of(Class<?>... classes) {
return new UserConfigurations(Arrays.asList(classes));
}
}

@ -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<Class<?>> classes) {
super(classes);
}
@Override
protected Configurations merge(Set<Class<?>> mergedClasses) {
return new TestConfigurations(mergedClasses);
}
}
@Order(Ordered.LOWEST_PRECEDENCE)
static class TestSortedConfigurations extends Configurations {
protected TestSortedConfigurations(Collection<Class<?>> classes) {
super(classes);
}
@Override
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
ArrayList<Class<?>> sorted = new ArrayList<>(classes);
sorted.sort(Comparator.comparing(ClassUtils::getShortName));
return sorted;
}
@Override
protected Configurations merge(Set<Class<?>> mergedClasses) {
return new TestSortedConfigurations(mergedClasses);
}
}
}

@ -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);
}
}
Loading…
Cancel
Save