Provide support for deprecated auto-configuration classes

Support `AutoConfiguration.replacements` file that can be placed
alongside an `AutoConfiguration.imports` to replace deprecated
auto-configurations.

Closes gh-14860
This commit is contained in:
Phillip Webb 2024-09-09 12:40:52 -07:00
parent ddd0d898c2
commit 72588fcda7
16 changed files with 569 additions and 61 deletions

View File

@ -86,6 +86,8 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
private final Class<?> autoConfigurationAnnotation;
private ConfigurableListableBeanFactory beanFactory;
private Environment environment;
@ -96,6 +98,17 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
private volatile ConfigurationClassFilter configurationClassFilter;
private volatile AutoConfigurationReplacements autoConfigurationReplacements;
public AutoConfigurationImportSelector() {
this(null);
}
AutoConfigurationImportSelector(Class<?> autoConfigurationAnnotation) {
this.autoConfigurationAnnotation = (autoConfigurationAnnotation != null) ? autoConfigurationAnnotation
: AutoConfiguration.class;
}
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
@ -179,11 +192,12 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation,
getBeanClassLoader());
List<String> configurations = importCandidates.getCandidates();
Assert.notEmpty(configurations,
"No auto configuration classes found in "
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
"No auto configuration classes found in " + "META-INF/spring/"
+ this.autoConfigurationAnnotation.getName() + ".imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
@ -227,7 +241,7 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(asList(attributes, "excludeName"));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
return getAutoConfigurationReplacements().replaceAll(excluded);
}
/**
@ -268,6 +282,16 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
return configurationClassFilter;
}
private AutoConfigurationReplacements getAutoConfigurationReplacements() {
AutoConfigurationReplacements autoConfigurationReplacements = this.autoConfigurationReplacements;
if (autoConfigurationReplacements == null) {
autoConfigurationReplacements = AutoConfigurationReplacements.load(this.autoConfigurationAnnotation,
this.beanClassLoader);
this.autoConfigurationReplacements = autoConfigurationReplacements;
}
return autoConfigurationReplacements;
}
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
@ -409,6 +433,8 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
private AutoConfigurationMetadata autoConfigurationMetadata;
private AutoConfigurationReplacements autoConfigurationReplacements;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
@ -430,7 +456,15 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
AutoConfigurationImportSelector autoConfigurationImportSelector = (AutoConfigurationImportSelector) deferredImportSelector;
AutoConfigurationReplacements autoConfigurationReplacements = autoConfigurationImportSelector
.getAutoConfigurationReplacements();
Assert.state(
this.autoConfigurationReplacements == null
|| this.autoConfigurationReplacements.equals(autoConfigurationReplacements),
"Auto-configuration replacements must be the same for each call to process");
this.autoConfigurationReplacements = autoConfigurationReplacements;
AutoConfigurationEntry autoConfigurationEntry = autoConfigurationImportSelector
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
@ -452,7 +486,6 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
.flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
processedConfigurations.removeAll(allExclusions);
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
.toList();
@ -467,7 +500,8 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
private List<String> sortAutoConfigurations(Set<String> configurations,
AutoConfigurationMetadata autoConfigurationMetadata) {
return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata)
return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata,
this.autoConfigurationReplacements::replace)
.getInPriorityOrder(configurations);
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2012-2024 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
*
* https://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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.core.io.UrlResource;
import org.springframework.util.Assert;
/**
* Contains auto-configuration replacements used to handle deprecated or moved
* auto-configurations which may still be referenced by
* {@link AutoConfigureBefore @AutoConfigureBefore},
* {@link AutoConfigureAfter @AutoConfigureAfter} or exclusions.
*
* @author Phillip Webb
*/
final class AutoConfigurationReplacements {
private static final String LOCATION = "META-INF/spring/%s.replacements";
private final Map<String, String> replacements;
private AutoConfigurationReplacements(Map<String, String> replacements) {
this.replacements = Map.copyOf(replacements);
}
Set<String> replaceAll(Set<String> classNames) {
Set<String> replaced = new LinkedHashSet<>(classNames.size());
for (String className : classNames) {
replaced.add(replace(className));
}
return replaced;
}
String replace(String className) {
return this.replacements.getOrDefault(className, className);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.replacements.equals(((AutoConfigurationReplacements) obj).replacements);
}
@Override
public int hashCode() {
return this.replacements.hashCode();
}
/**
* Loads the relocations from the classpath. Relactions are stored in files named
* {@code META-INF/spring/full-qualified-annotation-name.replacements} on the
* classpath. The file is loaded using {@link Properties#load(java.io.InputStream)}
* with each entry containing an auto-configuration class name as the key and the
* replacement class name as the value.
* @param annotation annotation to load
* @param classLoader class loader to use for loading
* @return list of names of annotated classes
*/
static AutoConfigurationReplacements load(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
Map<String, String> replacements = new HashMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
replacements.putAll(readReplacements(url));
}
return new AutoConfigurationReplacements(replacements);
}
private static ClassLoader decideClassloader(ClassLoader classLoader) {
if (classLoader == null) {
return ImportCandidates.class.getClassLoader();
}
return classLoader;
}
private static Enumeration<URL> findUrlsInClasspath(ClassLoader classLoader, String location) {
try {
return classLoader.getResources(location);
}
catch (IOException ex) {
throw new IllegalArgumentException("Failed to load configurations from location [" + location + "]", ex);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, String> readReplacements(URL url) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
Properties properties = new Properties();
properties.load(reader);
return (Map) properties;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load replacements from location [" + url + "]", ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -27,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.UnaryOperator;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
@ -47,11 +49,14 @@ class AutoConfigurationSorter {
private final AutoConfigurationMetadata autoConfigurationMetadata;
private final UnaryOperator<String> replacementMapper;
AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory,
AutoConfigurationMetadata autoConfigurationMetadata) {
AutoConfigurationMetadata autoConfigurationMetadata, UnaryOperator<String> replacementMapper) {
Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null");
this.metadataReaderFactory = metadataReaderFactory;
this.autoConfigurationMetadata = autoConfigurationMetadata;
this.replacementMapper = replacementMapper;
}
List<String> getInPriorityOrder(Collection<String> classNames) {
@ -108,7 +113,7 @@ class AutoConfigurationSorter {
() -> "AutoConfigure cycle detected between " + current + " and " + after);
}
private static class AutoConfigurationClasses {
private class AutoConfigurationClasses {
private final Map<String, AutoConfigurationClass> classes = new LinkedHashMap<>();
@ -157,7 +162,7 @@ class AutoConfigurationSorter {
}
private static class AutoConfigurationClass {
private class AutoConfigurationClass {
private final String className;
@ -192,20 +197,36 @@ class AutoConfigurationSorter {
Set<String> getBefore() {
if (this.before == null) {
this.before = (wasProcessed() ? this.autoConfigurationMetadata.getSet(this.className,
"AutoConfigureBefore", Collections.emptySet()) : getAnnotationValue(AutoConfigureBefore.class));
this.before = getClassNames("AutoConfigureBefore", AutoConfigureBefore.class);
}
return this.before;
}
Set<String> getAfter() {
if (this.after == null) {
this.after = (wasProcessed() ? this.autoConfigurationMetadata.getSet(this.className,
"AutoConfigureAfter", Collections.emptySet()) : getAnnotationValue(AutoConfigureAfter.class));
this.after = getClassNames("AutoConfigureAfter", AutoConfigureAfter.class);
}
return this.after;
}
private Set<String> getClassNames(String metadataKey, Class<? extends Annotation> annotation) {
Set<String> annotationValue = wasProcessed()
? this.autoConfigurationMetadata.getSet(this.className, metadataKey, Collections.emptySet())
: getAnnotationValue(annotation);
return applyReplacements(annotationValue);
}
private Set<String> applyReplacements(Set<String> values) {
if (AutoConfigurationSorter.this.replacementMapper == null) {
return values;
}
Set<String> replaced = new LinkedHashSet<>(values);
for (String value : values) {
replaced.add(AutoConfigurationSorter.this.replacementMapper.apply(value));
}
return replaced;
}
private int getOrder() {
if (wasProcessed()) {
return this.autoConfigurationMetadata.getInteger(this.className, "AutoConfigureOrder",

View File

@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.springframework.boot.context.annotation.Configurations;
@ -36,22 +37,33 @@ import org.springframework.util.ClassUtils;
*/
public class AutoConfigurations extends Configurations implements Ordered {
private static final AutoConfigurationSorter SORTER = new AutoConfigurationSorter(new SimpleMetadataReaderFactory(),
null);
private static final SimpleMetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
private static final int ORDER = AutoConfigurationImportSelector.ORDER;
static final AutoConfigurationReplacements replacements = AutoConfigurationReplacements
.load(AutoConfiguration.class, null);
private final UnaryOperator<String> replacementMapper;
protected AutoConfigurations(Collection<Class<?>> classes) {
super(classes);
this(replacements::replace, classes);
}
@Override
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
List<String> names = classes.stream().map(Class::getName).toList();
List<String> sorted = SORTER.getInPriorityOrder(names);
AutoConfigurations(UnaryOperator<String> replacementMapper, Collection<Class<?>> classes) {
super(sorter(replacementMapper), classes);
this.replacementMapper = replacementMapper;
}
private static UnaryOperator<Collection<Class<?>>> sorter(UnaryOperator<String> replacementMapper) {
AutoConfigurationSorter sorter = new AutoConfigurationSorter(metadataReaderFactory, null, replacementMapper);
return (classes) -> {
List<String> names = classes.stream().map(Class::getName).map(replacementMapper::apply).toList();
List<String> sorted = sorter.getInPriorityOrder(names);
return sorted.stream()
.map((className) -> ClassUtils.resolveClassName(className, null))
.collect(Collectors.toCollection(ArrayList::new));
};
}
@Override
@ -61,7 +73,7 @@ public class AutoConfigurations extends Configurations implements Ordered {
@Override
protected AutoConfigurations merge(Set<Class<?>> mergedClasses) {
return new AutoConfigurations(mergedClasses);
return new AutoConfigurations(this.replacementMapper, mergedClasses);
}
public static AutoConfigurations of(Class<?>... classes) {

View File

@ -16,15 +16,22 @@
package org.springframework.boot.autoconfigure;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@ -34,6 +41,8 @@ import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DeferredImportSelector.Group;
import org.springframework.context.annotation.DeferredImportSelector.Group.Entry;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.mock.env.MockEnvironment;
@ -50,7 +59,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class AutoConfigurationImportSelectorTests {
private final TestAutoConfigurationImportSelector importSelector = new TestAutoConfigurationImportSelector();
private final TestAutoConfigurationImportSelector importSelector = new TestAutoConfigurationImportSelector(null);
private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory();
@ -60,9 +69,7 @@ class AutoConfigurationImportSelectorTests {
@BeforeEach
void setup() {
this.importSelector.setBeanFactory(this.beanFactory);
this.importSelector.setEnvironment(this.environment);
this.importSelector.setResourceLoader(new DefaultResourceLoader());
setupImportSelector(this.importSelector);
}
@Test
@ -151,6 +158,17 @@ class AutoConfigurationImportSelectorTests {
ThymeleafAutoConfiguration.class.getName());
}
@Test
void removedExclusionsAreApplied() {
TestAutoConfigurationImportSelector importSelector = new TestAutoConfigurationImportSelector(
TestAutoConfiguration.class);
setupImportSelector(importSelector);
AnnotationMetadata metadata = AnnotationMetadata.introspect(BasicEnableAutoConfiguration.class);
assertThat(importSelector.selectImports(metadata)).contains(ReplacementAutoConfiguration.class.getName());
this.environment.setProperty("spring.autoconfigure.exclude", DeprecatedAutoConfiguration.class.getName());
assertThat(importSelector.selectImports(metadata)).doesNotContain(ReplacementAutoConfiguration.class.getName());
}
@Test
void nonAutoConfigurationClassExclusionsShouldThrowException() {
assertThatIllegalStateException()
@ -208,6 +226,22 @@ class AutoConfigurationImportSelectorTests {
assertThat(this.importSelector.getExclusionFilter().test("com.example.C")).isTrue();
}
@Test
void soringConsidersReplacements() {
TestAutoConfigurationImportSelector importSelector = new TestAutoConfigurationImportSelector(
TestAutoConfiguration.class);
setupImportSelector(importSelector);
AnnotationMetadata metadata = AnnotationMetadata.introspect(BasicEnableAutoConfiguration.class);
assertThat(importSelector.selectImports(metadata)).containsExactly(
AfterDeprecatedAutoConfiguration.class.getName(), ReplacementAutoConfiguration.class.getName());
Group group = BeanUtils.instantiateClass(importSelector.getImportGroup());
((BeanFactoryAware) group).setBeanFactory(this.beanFactory);
group.process(metadata, importSelector);
Stream<Entry> imports = StreamSupport.stream(group.selectImports().spliterator(), false);
assertThat(imports.map(Entry::getImportClassName)).containsExactly(ReplacementAutoConfiguration.class.getName(),
AfterDeprecatedAutoConfiguration.class.getName());
}
private String[] selectImports(Class<?> source) {
return this.importSelector.selectImports(AnnotationMetadata.introspect(source));
}
@ -216,10 +250,20 @@ class AutoConfigurationImportSelectorTests {
return ImportCandidates.load(AutoConfiguration.class, getClass().getClassLoader()).getCandidates();
}
private void setupImportSelector(TestAutoConfigurationImportSelector importSelector) {
importSelector.setBeanFactory(this.beanFactory);
importSelector.setEnvironment(this.environment);
importSelector.setResourceLoader(new DefaultResourceLoader());
}
private final class TestAutoConfigurationImportSelector extends AutoConfigurationImportSelector {
private AutoConfigurationImportEvent lastEvent;
TestAutoConfigurationImportSelector(Class<?> autoConfigurationAnnotation) {
super(autoConfigurationAnnotation);
}
@Override
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
return AutoConfigurationImportSelectorTests.this.filters;
@ -320,4 +364,23 @@ class AutoConfigurationImportSelectorTests {
}
static class DeprecatedAutoConfiguration {
}
static class ReplacementAutoConfiguration {
}
@AutoConfigureAfter(DeprecatedAutoConfiguration.class)
static class AfterDeprecatedAutoConfiguration {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TestAutoConfiguration {
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2012-2024 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
*
* https://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.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AutoConfigurationReplacements}.
*
* @author Phillip Webb
*/
class AutoConfigurationReplacementsTests {
private final AutoConfigurationReplacements replacements = AutoConfigurationReplacements
.load(TestAutoConfigurationReplacements.class, null);
@Test
void replaceWhenMatchReplacesClassName() {
assertThat(this.replacements.replace("com.example.A1")).isEqualTo("com.example.A2");
}
@Test
void replaceWhenNoMatchReturnsOriginalClassName() {
assertThat(this.replacements.replace("com.example.Z1")).isEqualTo("com.example.Z1");
}
@Test
void replaceAllReplacesAllMatching() {
Set<String> classNames = new LinkedHashSet<>(
List.of("com.example.A1", "com.example.B1", "com.example.Y1", "com.example.Z1"));
assertThat(this.replacements.replaceAll(classNames)).containsExactly("com.example.A2", "com.example.B2",
"com.example.Y1", "com.example.Z1");
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TestAutoConfigurationReplacements {
}
}

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.UnaryOperator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -62,10 +63,14 @@ class AutoConfigurationSorterTests {
private static final String A3 = AutoConfigureA3.class.getName();
private static final String A_WITH_REPLACED = AutoConfigureAWithReplaced.class.getName();
private static final String B = AutoConfigureB.class.getName();
private static final String B2 = AutoConfigureB2.class.getName();
private static final String B_WITH_REPLACED = AutoConfigureBWithReplaced.class.getName();
private static final String C = AutoConfigureC.class.getName();
private static final String D = AutoConfigureD.class.getName();
@ -86,13 +91,16 @@ class AutoConfigurationSorterTests {
private static final String Z2 = AutoConfigureZ2.class.getName();
private static final UnaryOperator<String> REPLACEMENT_MAPPER = (name) -> name.replace("Deprecated", "");
private AutoConfigurationSorter sorter;
private AutoConfigurationMetadata autoConfigurationMetadata = mock(AutoConfigurationMetadata.class);
@BeforeEach
void setup() {
this.sorter = new AutoConfigurationSorter(new SkipCycleMetadataReaderFactory(), this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(new SkipCycleMetadataReaderFactory(), this.autoConfigurationMetadata,
REPLACEMENT_MAPPER);
}
@Test
@ -117,11 +125,17 @@ class AutoConfigurationSorterTests {
void byAutoConfigureAfterAliasForWithProperties() throws Exception {
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory();
this.autoConfigurationMetadata = getAutoConfigurationMetadata(A3, B2, C);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata, REPLACEMENT_MAPPER);
List<String> actual = getInPriorityOrder(A3, B2, C);
assertThat(actual).containsExactly(C, B2, A3);
}
@Test
void byAutoConfigureAfterWithDeprecated() {
List<String> actual = getInPriorityOrder(A_WITH_REPLACED, B_WITH_REPLACED, C);
assertThat(actual).containsExactly(C, B_WITH_REPLACED, A_WITH_REPLACED);
}
@Test
void byAutoConfigureBefore() {
List<String> actual = getInPriorityOrder(X, Y, Z);
@ -138,7 +152,7 @@ class AutoConfigurationSorterTests {
void byAutoConfigureBeforeAliasForWithProperties() throws Exception {
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory();
this.autoConfigurationMetadata = getAutoConfigurationMetadata(X, Y2, Z2);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata, REPLACEMENT_MAPPER);
List<String> actual = getInPriorityOrder(X, Y2, Z2);
assertThat(actual).containsExactly(Z2, Y2, X);
}
@ -175,7 +189,8 @@ class AutoConfigurationSorterTests {
@Test
void byAutoConfigureAfterWithCycle() {
this.sorter = new AutoConfigurationSorter(new CachingMetadataReaderFactory(), this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(new CachingMetadataReaderFactory(), this.autoConfigurationMetadata,
REPLACEMENT_MAPPER);
assertThatIllegalStateException().isThrownBy(() -> getInPriorityOrder(A, B, C, D))
.withMessageContaining("AutoConfigure cycle detected");
}
@ -184,7 +199,7 @@ class AutoConfigurationSorterTests {
void usesAnnotationPropertiesWhenPossible() throws Exception {
MetadataReaderFactory readerFactory = new SkipCycleMetadataReaderFactory();
this.autoConfigurationMetadata = getAutoConfigurationMetadata(A2, B, C, W2, X);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata, REPLACEMENT_MAPPER);
List<String> actual = getInPriorityOrder(A2, B, C, W2, X);
assertThat(actual).containsExactly(C, W2, B, A2, X);
}
@ -193,7 +208,7 @@ class AutoConfigurationSorterTests {
void useAnnotationWithNoDirectLink() throws Exception {
MetadataReaderFactory readerFactory = new SkipCycleMetadataReaderFactory();
this.autoConfigurationMetadata = getAutoConfigurationMetadata(A, B, E);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata, REPLACEMENT_MAPPER);
List<String> actual = getInPriorityOrder(A, E);
assertThat(actual).containsExactly(E, A);
}
@ -202,7 +217,7 @@ class AutoConfigurationSorterTests {
void useAnnotationWithNoDirectLinkAndCycle() throws Exception {
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory();
this.autoConfigurationMetadata = getAutoConfigurationMetadata(A, B, D);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata);
this.sorter = new AutoConfigurationSorter(readerFactory, this.autoConfigurationMetadata, REPLACEMENT_MAPPER);
assertThatIllegalStateException().isThrownBy(() -> getInPriorityOrder(D, B))
.withMessageContaining("AutoConfigure cycle detected");
}
@ -307,6 +322,11 @@ class AutoConfigurationSorterTests {
}
@AutoConfigureAfter(AutoConfigureBWithReplaced.class)
public static class AutoConfigureAWithReplaced {
}
@AutoConfigureAfter({ AutoConfigureC.class, AutoConfigureD.class, AutoConfigureE.class })
static class AutoConfigureB {
@ -317,10 +337,21 @@ class AutoConfigurationSorterTests {
}
@AutoConfigureAfter({ DeprecatedAutoConfigureC.class, AutoConfigureD.class, AutoConfigureE.class })
public static class AutoConfigureBWithReplaced {
}
static class AutoConfigureC {
}
// @DeprecatedAutoConfiguration(replacement =
// "org.springframework.boot.autoconfigure.AutoConfigurationSorterTests$AutoConfigureC")
public static class DeprecatedAutoConfigureC {
}
@AutoConfigureAfter(AutoConfigureA.class)
static class AutoConfigureD {
@ -354,6 +385,12 @@ class AutoConfigurationSorterTests {
}
// @DeprecatedAutoConfiguration(replacement =
// "org.springframework.boot.autoconfigure.AutoConfigurationSorterTests$AutoConfigureY")
public static class DeprecatedAutoConfigureY {
}
@AutoConfigureBefore(AutoConfigureY.class)
static class AutoConfigureZ {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2024 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.
@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.annotation.Configurations;
@ -36,6 +38,26 @@ class AutoConfigurationsTests {
AutoConfigureA.class);
}
@Test
void whenHasReplacementForAutoConfigureAfterShouldCreateOrderedConfigurations() {
Configurations configurations = new AutoConfigurations(this::replaceB,
Arrays.asList(AutoConfigureA.class, AutoConfigureB2.class));
assertThat(Configurations.getClasses(configurations)).containsExactly(AutoConfigureB2.class,
AutoConfigureA.class);
}
@Test
void whenHasReplacementForClassShouldReplaceClass() {
Configurations configurations = new AutoConfigurations(this::replaceB,
Arrays.asList(AutoConfigureA.class, AutoConfigureB.class));
assertThat(Configurations.getClasses(configurations)).containsExactly(AutoConfigureB2.class,
AutoConfigureA.class);
}
private String replaceB(String className) {
return (!AutoConfigureB.class.getName().equals(className)) ? className : AutoConfigureB2.class.getName();
}
@AutoConfigureAfter(AutoConfigureB.class)
static class AutoConfigureA {
@ -45,4 +67,8 @@ class AutoConfigurationsTests {
}
static class AutoConfigureB2 {
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2024 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.
@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.function.UnaryOperator;
import org.springframework.core.type.classreading.MetadataReaderFactory;
@ -29,8 +30,9 @@ import org.springframework.core.type.classreading.MetadataReaderFactory;
*/
public class TestAutoConfigurationSorter extends AutoConfigurationSorter {
public TestAutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory) {
super(metadataReaderFactory, AutoConfigurationMetadataLoader.loadMetadata(new Properties()));
public TestAutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory,
UnaryOperator<String> replacementMapper) {
super(metadataReaderFactory, AutoConfigurationMetadataLoader.loadMetadata(new Properties()), replacementMapper);
}
@Override

View File

@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$AfterDeprecatedAutoConfiguration
org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$ReplacementAutoConfiguration

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$DeprecatedAutoConfiguration=\
org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$ReplacementAutoConfiguration

View File

@ -52,6 +52,26 @@ The order in which those beans are subsequently created is unaffected and is det
[[features.developing-auto-configuration.locating-auto-configuration-candidates.deprecating]]
=== Deprecating and Replacing Auto-configuration Classes
You may need to occasionally deprecate auto-configuration classes and offer an alternative.
For example, you may want to change the package name where your auto-configuration class resides.
Since auto-configuration classes may be referenced in `before`/`after` ordering and `excludes`, you'll need to add an additional file that tells Spring Boot how to deal with replacements.
To define replacements, create a `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.replacements` file indicating the link between the old class and the new one.
For example:
[source]
----
com.mycorp.libx.autoconfigure.LibXAutoConfiguration=com.mycorp.libx.autoconfigure.core.LibXAutoConfiguration
----
NOTE: The `AutoConfiguration.imports` file should also be updated to _only_ reference the replacement class.
[[features.developing-auto-configuration.condition-annotations]]
== Condition Annotations

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -25,6 +25,7 @@ import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -60,11 +61,32 @@ public abstract class Configurations {
private static final Comparator<Object> COMPARATOR = OrderComparator.INSTANCE
.thenComparing((other) -> other.getClass().getName());
private final UnaryOperator<Collection<Class<?>>> sorter;
private final Set<Class<?>> classes;
/**
* Create a new {@link Configurations} instance.
* @param classes the configuration classes
*/
protected Configurations(Collection<Class<?>> classes) {
Assert.notNull(classes, "Classes must not be null");
Collection<Class<?>> sorted = sort(classes);
this.sorter = null;
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
}
/**
* Create a new {@link Configurations} instance.
* @param sorter a {@link UnaryOperator} used to sort the configurations
* @param classes the configuration classes
* @since 3.4.0
*/
protected Configurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes) {
Assert.notNull(sorter, "Sorter must not be null");
Assert.notNull(classes, "Classes must not be null");
Collection<Class<?>> sorted = sorter.apply(classes);
this.sorter = sorter;
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
}
@ -72,7 +94,10 @@ public abstract class Configurations {
* Sort configuration classes into the order that they should be applied.
* @param classes the classes to sort
* @return a sorted set of classes
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
* {@link #Configurations(UnaryOperator, Collection)}
*/
@Deprecated(since = "3.4.0", forRemoval = true)
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
return classes;
}
@ -90,6 +115,9 @@ public abstract class Configurations {
protected Configurations merge(Configurations other) {
Set<Class<?>> mergedClasses = new LinkedHashSet<>(getClasses());
mergedClasses.addAll(other.getClasses());
if (this.sorter != null) {
mergedClasses = new LinkedHashSet<>(this.sorter.apply(mergedClasses));
}
return merge(mergedClasses);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@ -67,9 +67,8 @@ public final class ImportCandidates implements Iterable<String> {
}
/**
* Loads the names of import candidates from the classpath.
*
* The names of the import candidates are stored in files named
* Loads the names of import candidates from the classpath. The names of the import
* candidates are stored in files named
* {@code META-INF/spring/full-qualified-annotation-name.imports} on the classpath.
* Every line contains the full qualified name of the candidate class. Comments are
* supported using the # character.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.function.UnaryOperator;
import org.junit.jupiter.api.Test;
@ -43,32 +44,61 @@ class ConfigurationsTests {
@Test
void createWhenClassesIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new TestConfigurations(null))
assertThatIllegalArgumentException().isThrownBy(() -> new TestConfigurations((Collection<Class<?>>) null))
.withMessageContaining("Classes must not be null");
}
@Test
void createShouldSortClasses() {
TestSortedConfigurations configurations = new TestSortedConfigurations(
@Deprecated(since = "3.4.0", forRemoval = true)
void createShouldSortClassesUsingSortMethod() {
TestDeprecatedSortedConfigurations configurations = new TestDeprecatedSortedConfigurations(
Arrays.asList(OutputStream.class, InputStream.class));
assertThat(configurations.getClasses()).containsExactly(InputStream.class, OutputStream.class);
}
@Test
void getClassesShouldMergeByClassAndSort() {
Configurations c1 = new TestSortedConfigurations(Arrays.asList(OutputStream.class, InputStream.class));
@Deprecated(since = "3.4.0", forRemoval = true)
void getClassesShouldMergeByClassAndSortUsingSortMethod() {
Configurations c1 = new TestDeprecatedSortedConfigurations(
Arrays.asList(OutputStream.class, InputStream.class));
Configurations c2 = new TestConfigurations(Collections.singletonList(Short.class));
Configurations c3 = new TestSortedConfigurations(Arrays.asList(String.class, Integer.class));
Configurations c3 = new TestDeprecatedSortedConfigurations(Arrays.asList(String.class, Integer.class));
Configurations c4 = new TestConfigurations(Arrays.asList(Long.class, Byte.class));
Class<?>[] classes = Configurations.getClasses(c1, c2, c3, c4);
assertThat(classes).containsExactly(Short.class, Long.class, Byte.class, InputStream.class, Integer.class,
OutputStream.class, String.class);
}
@Test
void createShouldSortClasses() {
TestConfigurations configurations = new TestConfigurations(Sorter.instance, OutputStream.class,
InputStream.class);
assertThat(configurations.getClasses()).containsExactly(InputStream.class, OutputStream.class);
}
@Test
void getClassesShouldMergeByClassAndSort() {
Configurations c1 = new TestSortedConfigurations(OutputStream.class, InputStream.class);
Configurations c2 = new TestConfigurations(Short.class);
Configurations c3 = new TestSortedConfigurations(String.class, Integer.class);
Configurations c4 = new TestConfigurations(Long.class, Byte.class);
Class<?>[] classes = Configurations.getClasses(c1, c2, c3, c4);
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) {
TestConfigurations(Class<?>... classes) {
this(Arrays.asList(classes));
}
TestConfigurations(UnaryOperator<Collection<Class<?>>> sorter, Class<?>... classes) {
super(sorter, Arrays.asList(classes));
}
TestConfigurations(Collection<Class<?>> classes) {
super(classes);
}
@ -82,15 +112,12 @@ class ConfigurationsTests {
@Order(Ordered.LOWEST_PRECEDENCE)
static class TestSortedConfigurations extends Configurations {
protected TestSortedConfigurations(Collection<Class<?>> classes) {
super(classes);
protected TestSortedConfigurations(Class<?>... classes) {
this(Arrays.asList(classes));
}
@Override
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
ArrayList<Class<?>> sorted = new ArrayList<>(classes);
sorted.sort(Comparator.comparing(ClassUtils::getShortName));
return sorted;
protected TestSortedConfigurations(Collection<Class<?>> classes) {
super(Sorter.instance, classes);
}
@Override
@ -100,4 +127,38 @@ class ConfigurationsTests {
}
@Order(Ordered.LOWEST_PRECEDENCE)
@SuppressWarnings("removal")
static class TestDeprecatedSortedConfigurations extends Configurations {
protected TestDeprecatedSortedConfigurations(Collection<Class<?>> classes) {
super(classes);
}
@Override
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
return Sorter.instance.apply(classes);
}
@Override
protected Configurations merge(Set<Class<?>> mergedClasses) {
return new TestDeprecatedSortedConfigurations(mergedClasses);
}
}
static class Sorter implements UnaryOperator<Collection<Class<?>>> {
static final Sorter instance = new Sorter();
@Override
public Collection<Class<?>> apply(Collection<Class<?>> classes) {
ArrayList<Class<?>> sorted = new ArrayList<>(classes);
sorted.sort(Comparator.comparing(ClassUtils::getShortName));
return sorted;
}
}
}