From 72588fcda78407429bcc51472c0ad06cc4747b33 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Sep 2024 12:40:52 -0700 Subject: [PATCH] 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 --- .../AutoConfigurationImportSelector.java | 48 ++++++- .../AutoConfigurationReplacements.java | 133 ++++++++++++++++++ .../AutoConfigurationSorter.java | 37 +++-- .../autoconfigure/AutoConfigurations.java | 34 +++-- .../AutoConfigurationImportSelectorTests.java | 71 +++++++++- .../AutoConfigurationReplacementsTests.java | 65 +++++++++ .../AutoConfigurationSorterTests.java | 51 ++++++- .../AutoConfigurationsTests.java | 28 +++- .../TestAutoConfigurationSorter.java | 8 +- ...electorTests$TestAutoConfiguration.imports | 3 + ...orTests$TestAutoConfiguration.replacements | 2 + ...AutoConfigurationReplacements.replacements | 2 + .../developing-auto-configuration.adoc | 20 +++ .../context/annotation/Configurations.java | 30 +++- .../context/annotation/ImportCandidates.java | 7 +- .../annotation/ConfigurationsTests.java | 91 ++++++++++-- 16 files changed, 569 insertions(+), 61 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacements.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacementsTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.imports create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.replacements create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationReplacementsTests$TestAutoConfigurationReplacements.replacements diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java index 025b1aedf8d..e3da084cf8f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java @@ -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 getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { - ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()); + ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, + getBeanClassLoader()); List 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 List removeDuplicates(List 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 sortAutoConfigurations(Set configurations, AutoConfigurationMetadata autoConfigurationMetadata) { - return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata) + return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata, + this.autoConfigurationReplacements::replace) .getInPriorityOrder(configurations); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacements.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacements.java new file mode 100644 index 00000000000..88846230c0e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacements.java @@ -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 replacements; + + private AutoConfigurationReplacements(Map replacements) { + this.replacements = Map.copyOf(replacements); + } + + Set replaceAll(Set classNames) { + Set 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 urls = findUrlsInClasspath(classLoaderToUse, location); + Map 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 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 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); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index 271fc11087d..043da47b27c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -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 replacementMapper; + AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory, - AutoConfigurationMetadata autoConfigurationMetadata) { + AutoConfigurationMetadata autoConfigurationMetadata, UnaryOperator replacementMapper) { Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null"); this.metadataReaderFactory = metadataReaderFactory; this.autoConfigurationMetadata = autoConfigurationMetadata; + this.replacementMapper = replacementMapper; } List getInPriorityOrder(Collection classNames) { @@ -108,7 +113,7 @@ class AutoConfigurationSorter { () -> "AutoConfigure cycle detected between " + current + " and " + after); } - private static class AutoConfigurationClasses { + private class AutoConfigurationClasses { private final Map 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 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 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 getClassNames(String metadataKey, Class annotation) { + Set annotationValue = wasProcessed() + ? this.autoConfigurationMetadata.getSet(this.className, metadataKey, Collections.emptySet()) + : getAnnotationValue(annotation); + return applyReplacements(annotationValue); + } + + private Set applyReplacements(Set values) { + if (AutoConfigurationSorter.this.replacementMapper == null) { + return values; + } + Set 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", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java index f5654ea4db3..9c0b22bf9e7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java @@ -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 replacementMapper; + protected AutoConfigurations(Collection> classes) { - super(classes); + this(replacements::replace, classes); } - @Override - protected Collection> sort(Collection> classes) { - List names = classes.stream().map(Class::getName).toList(); - List sorted = SORTER.getInPriorityOrder(names); - return sorted.stream() - .map((className) -> ClassUtils.resolveClassName(className, null)) - .collect(Collectors.toCollection(ArrayList::new)); + AutoConfigurations(UnaryOperator replacementMapper, Collection> classes) { + super(sorter(replacementMapper), classes); + this.replacementMapper = replacementMapper; + } + + private static UnaryOperator>> sorter(UnaryOperator replacementMapper) { + AutoConfigurationSorter sorter = new AutoConfigurationSorter(metadataReaderFactory, null, replacementMapper); + return (classes) -> { + List names = classes.stream().map(Class::getName).map(replacementMapper::apply).toList(); + List 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> mergedClasses) { - return new AutoConfigurations(mergedClasses); + return new AutoConfigurations(this.replacementMapper, mergedClasses); } public static AutoConfigurations of(Class... classes) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java index b74a7cb9933..41ced26ff2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java @@ -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 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 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 { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacementsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacementsTests.java new file mode 100644 index 00000000000..30ae61950a1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationReplacementsTests.java @@ -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 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 { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java index 93a00ba89cc..9352bbc7f3b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java @@ -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 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 actual = getInPriorityOrder(A3, B2, C); assertThat(actual).containsExactly(C, B2, A3); } + @Test + void byAutoConfigureAfterWithDeprecated() { + List actual = getInPriorityOrder(A_WITH_REPLACED, B_WITH_REPLACED, C); + assertThat(actual).containsExactly(C, B_WITH_REPLACED, A_WITH_REPLACED); + } + @Test void byAutoConfigureBefore() { List 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 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 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 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 { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java index 3b7435594a4..dcfb3da0d7f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java @@ -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 { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java index 728fbb110d4..c413059ea8d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/TestAutoConfigurationSorter.java @@ -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 replacementMapper) { + super(metadataReaderFactory, AutoConfigurationMetadataLoader.loadMetadata(new Properties()), replacementMapper); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.imports new file mode 100644 index 00000000000..3d625bcf410 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.imports @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$AfterDeprecatedAutoConfiguration +org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$ReplacementAutoConfiguration + diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.replacements b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.replacements new file mode 100644 index 00000000000..b63c6b88df3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$TestAutoConfiguration.replacements @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$DeprecatedAutoConfiguration=\ +org.springframework.boot.autoconfigure.AutoConfigurationImportSelectorTests$ReplacementAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationReplacementsTests$TestAutoConfigurationReplacements.replacements b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationReplacementsTests$TestAutoConfigurationReplacements.replacements new file mode 100644 index 00000000000..9924a4a0c10 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfigurationReplacementsTests$TestAutoConfigurationReplacements.replacements @@ -0,0 +1,2 @@ +com.example.A1=com.example.A2 +com.example.B1=com.example.B2 diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc index ee1c3551b5e..6ab3290e11d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc @@ -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 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java index 49031a6e0c0..8d86a545aad 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java @@ -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 COMPARATOR = OrderComparator.INSTANCE .thenComparing((other) -> other.getClass().getName()); + private final UnaryOperator>> sorter; + private final Set> classes; + /** + * Create a new {@link Configurations} instance. + * @param classes the configuration classes + */ protected Configurations(Collection> classes) { Assert.notNull(classes, "Classes must not be null"); Collection> 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>> sorter, Collection> classes) { + Assert.notNull(sorter, "Sorter must not be null"); + Assert.notNull(classes, "Classes must not be null"); + Collection> 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> sort(Collection> classes) { return classes; } @@ -90,6 +115,9 @@ public abstract class Configurations { protected Configurations merge(Configurations other) { Set> mergedClasses = new LinkedHashSet<>(getClasses()); mergedClasses.addAll(other.getClasses()); + if (this.sorter != null) { + mergedClasses = new LinkedHashSet<>(this.sorter.apply(mergedClasses)); + } return merge(mergedClasses); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/ImportCandidates.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/ImportCandidates.java index 59fe569e94f..5758a898d85 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/ImportCandidates.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/ImportCandidates.java @@ -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 { } /** - * 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. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java index 71b36adf9e0..23d50cd4c7f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/annotation/ConfigurationsTests.java @@ -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>) 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> classes) { + TestConfigurations(Class... classes) { + this(Arrays.asList(classes)); + } + + TestConfigurations(UnaryOperator>> sorter, Class... classes) { + super(sorter, Arrays.asList(classes)); + } + + TestConfigurations(Collection> classes) { super(classes); } @@ -82,15 +112,12 @@ class ConfigurationsTests { @Order(Ordered.LOWEST_PRECEDENCE) static class TestSortedConfigurations extends Configurations { - protected TestSortedConfigurations(Collection> classes) { - super(classes); + protected TestSortedConfigurations(Class... classes) { + this(Arrays.asList(classes)); } - @Override - protected Collection> sort(Collection> classes) { - ArrayList> sorted = new ArrayList<>(classes); - sorted.sort(Comparator.comparing(ClassUtils::getShortName)); - return sorted; + protected TestSortedConfigurations(Collection> 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> classes) { + super(classes); + } + + @Override + protected Collection> sort(Collection> classes) { + return Sorter.instance.apply(classes); + } + + @Override + protected Configurations merge(Set> mergedClasses) { + return new TestDeprecatedSortedConfigurations(mergedClasses); + } + + } + + static class Sorter implements UnaryOperator>> { + + static final Sorter instance = new Sorter(); + + @Override + public Collection> apply(Collection> classes) { + ArrayList> sorted = new ArrayList<>(classes); + sorted.sort(Comparator.comparing(ClassUtils::getShortName)); + return sorted; + + } + + } + }