Skip to content

Commit 72588fc

Browse files
committed
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
1 parent ddd0d89 commit 72588fc

16 files changed

+566
-58
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
8686

8787
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
8888

89+
private final Class<?> autoConfigurationAnnotation;
90+
8991
private ConfigurableListableBeanFactory beanFactory;
9092

9193
private Environment environment;
@@ -96,6 +98,17 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
9698

9799
private volatile ConfigurationClassFilter configurationClassFilter;
98100

101+
private volatile AutoConfigurationReplacements autoConfigurationReplacements;
102+
103+
public AutoConfigurationImportSelector() {
104+
this(null);
105+
}
106+
107+
AutoConfigurationImportSelector(Class<?> autoConfigurationAnnotation) {
108+
this.autoConfigurationAnnotation = (autoConfigurationAnnotation != null) ? autoConfigurationAnnotation
109+
: AutoConfiguration.class;
110+
}
111+
99112
@Override
100113
public String[] selectImports(AnnotationMetadata annotationMetadata) {
101114
if (!isEnabled(annotationMetadata)) {
@@ -179,11 +192,12 @@ protected Class<?> getAnnotationClass() {
179192
* @return a list of candidate configurations
180193
*/
181194
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
182-
ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());
195+
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation,
196+
getBeanClassLoader());
183197
List<String> configurations = importCandidates.getCandidates();
184198
Assert.notEmpty(configurations,
185-
"No auto configuration classes found in "
186-
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
199+
"No auto configuration classes found in " + "META-INF/spring/"
200+
+ this.autoConfigurationAnnotation.getName() + ".imports. If you "
187201
+ "are using a custom packaging, make sure that file is correct.");
188202
return configurations;
189203
}
@@ -227,7 +241,7 @@ protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttri
227241
excluded.addAll(asList(attributes, "exclude"));
228242
excluded.addAll(asList(attributes, "excludeName"));
229243
excluded.addAll(getExcludeAutoConfigurationsProperty());
230-
return excluded;
244+
return getAutoConfigurationReplacements().replaceAll(excluded);
231245
}
232246

233247
/**
@@ -268,6 +282,16 @@ private ConfigurationClassFilter getConfigurationClassFilter() {
268282
return configurationClassFilter;
269283
}
270284

285+
private AutoConfigurationReplacements getAutoConfigurationReplacements() {
286+
AutoConfigurationReplacements autoConfigurationReplacements = this.autoConfigurationReplacements;
287+
if (autoConfigurationReplacements == null) {
288+
autoConfigurationReplacements = AutoConfigurationReplacements.load(this.autoConfigurationAnnotation,
289+
this.beanClassLoader);
290+
this.autoConfigurationReplacements = autoConfigurationReplacements;
291+
}
292+
return autoConfigurationReplacements;
293+
}
294+
271295
protected final <T> List<T> removeDuplicates(List<T> list) {
272296
return new ArrayList<>(new LinkedHashSet<>(list));
273297
}
@@ -409,6 +433,8 @@ private static final class AutoConfigurationGroup
409433

410434
private AutoConfigurationMetadata autoConfigurationMetadata;
411435

436+
private AutoConfigurationReplacements autoConfigurationReplacements;
437+
412438
@Override
413439
public void setBeanClassLoader(ClassLoader classLoader) {
414440
this.beanClassLoader = classLoader;
@@ -430,7 +456,15 @@ public void process(AnnotationMetadata annotationMetadata, DeferredImportSelecto
430456
() -> String.format("Only %s implementations are supported, got %s",
431457
AutoConfigurationImportSelector.class.getSimpleName(),
432458
deferredImportSelector.getClass().getName()));
433-
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
459+
AutoConfigurationImportSelector autoConfigurationImportSelector = (AutoConfigurationImportSelector) deferredImportSelector;
460+
AutoConfigurationReplacements autoConfigurationReplacements = autoConfigurationImportSelector
461+
.getAutoConfigurationReplacements();
462+
Assert.state(
463+
this.autoConfigurationReplacements == null
464+
|| this.autoConfigurationReplacements.equals(autoConfigurationReplacements),
465+
"Auto-configuration replacements must be the same for each call to process");
466+
this.autoConfigurationReplacements = autoConfigurationReplacements;
467+
AutoConfigurationEntry autoConfigurationEntry = autoConfigurationImportSelector
434468
.getAutoConfigurationEntry(annotationMetadata);
435469
this.autoConfigurationEntries.add(autoConfigurationEntry);
436470
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
@@ -452,7 +486,6 @@ public Iterable<Entry> selectImports() {
452486
.flatMap(Collection::stream)
453487
.collect(Collectors.toCollection(LinkedHashSet::new));
454488
processedConfigurations.removeAll(allExclusions);
455-
456489
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
457490
.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
458491
.toList();
@@ -467,7 +500,8 @@ private AutoConfigurationMetadata getAutoConfigurationMetadata() {
467500

468501
private List<String> sortAutoConfigurations(Set<String> configurations,
469502
AutoConfigurationMetadata autoConfigurationMetadata) {
470-
return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata)
503+
return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata,
504+
this.autoConfigurationReplacements::replace)
471505
.getInPriorityOrder(configurations);
472506
}
473507

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure;
18+
19+
import java.io.BufferedReader;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.net.URL;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Enumeration;
25+
import java.util.HashMap;
26+
import java.util.LinkedHashSet;
27+
import java.util.Map;
28+
import java.util.Properties;
29+
import java.util.Set;
30+
31+
import org.springframework.boot.context.annotation.ImportCandidates;
32+
import org.springframework.core.io.UrlResource;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* Contains auto-configuration replacements used to handle deprecated or moved
37+
* auto-configurations which may still be referenced by
38+
* {@link AutoConfigureBefore @AutoConfigureBefore},
39+
* {@link AutoConfigureAfter @AutoConfigureAfter} or exclusions.
40+
*
41+
* @author Phillip Webb
42+
*/
43+
final class AutoConfigurationReplacements {
44+
45+
private static final String LOCATION = "META-INF/spring/%s.replacements";
46+
47+
private final Map<String, String> replacements;
48+
49+
private AutoConfigurationReplacements(Map<String, String> replacements) {
50+
this.replacements = Map.copyOf(replacements);
51+
}
52+
53+
Set<String> replaceAll(Set<String> classNames) {
54+
Set<String> replaced = new LinkedHashSet<>(classNames.size());
55+
for (String className : classNames) {
56+
replaced.add(replace(className));
57+
}
58+
return replaced;
59+
}
60+
61+
String replace(String className) {
62+
return this.replacements.getOrDefault(className, className);
63+
}
64+
65+
@Override
66+
public boolean equals(Object obj) {
67+
if (this == obj) {
68+
return true;
69+
}
70+
if (obj == null || getClass() != obj.getClass()) {
71+
return false;
72+
}
73+
return this.replacements.equals(((AutoConfigurationReplacements) obj).replacements);
74+
}
75+
76+
@Override
77+
public int hashCode() {
78+
return this.replacements.hashCode();
79+
}
80+
81+
/**
82+
* Loads the relocations from the classpath. Relactions are stored in files named
83+
* {@code META-INF/spring/full-qualified-annotation-name.replacements} on the
84+
* classpath. The file is loaded using {@link Properties#load(java.io.InputStream)}
85+
* with each entry containing an auto-configuration class name as the key and the
86+
* replacement class name as the value.
87+
* @param annotation annotation to load
88+
* @param classLoader class loader to use for loading
89+
* @return list of names of annotated classes
90+
*/
91+
static AutoConfigurationReplacements load(Class<?> annotation, ClassLoader classLoader) {
92+
Assert.notNull(annotation, "'annotation' must not be null");
93+
ClassLoader classLoaderToUse = decideClassloader(classLoader);
94+
String location = String.format(LOCATION, annotation.getName());
95+
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
96+
Map<String, String> replacements = new HashMap<>();
97+
while (urls.hasMoreElements()) {
98+
URL url = urls.nextElement();
99+
replacements.putAll(readReplacements(url));
100+
}
101+
return new AutoConfigurationReplacements(replacements);
102+
}
103+
104+
private static ClassLoader decideClassloader(ClassLoader classLoader) {
105+
if (classLoader == null) {
106+
return ImportCandidates.class.getClassLoader();
107+
}
108+
return classLoader;
109+
}
110+
111+
private static Enumeration<URL> findUrlsInClasspath(ClassLoader classLoader, String location) {
112+
try {
113+
return classLoader.getResources(location);
114+
}
115+
catch (IOException ex) {
116+
throw new IllegalArgumentException("Failed to load configurations from location [" + location + "]", ex);
117+
}
118+
}
119+
120+
@SuppressWarnings({ "unchecked", "rawtypes" })
121+
private static Map<String, String> readReplacements(URL url) {
122+
try (BufferedReader reader = new BufferedReader(
123+
new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
124+
Properties properties = new Properties();
125+
properties.load(reader);
126+
return (Map) properties;
127+
}
128+
catch (IOException ex) {
129+
throw new IllegalArgumentException("Unable to load replacements from location [" + url + "]", ex);
130+
}
131+
}
132+
133+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.autoconfigure;
1818

1919
import java.io.IOException;
20+
import java.lang.annotation.Annotation;
2021
import java.util.ArrayList;
2122
import java.util.Collection;
2223
import java.util.Collections;
@@ -27,6 +28,7 @@
2728
import java.util.Map;
2829
import java.util.Set;
2930
import java.util.TreeSet;
31+
import java.util.function.UnaryOperator;
3032

3133
import org.springframework.core.type.AnnotationMetadata;
3234
import org.springframework.core.type.classreading.MetadataReader;
@@ -47,11 +49,14 @@ class AutoConfigurationSorter {
4749

4850
private final AutoConfigurationMetadata autoConfigurationMetadata;
4951

52+
private final UnaryOperator<String> replacementMapper;
53+
5054
AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory,
51-
AutoConfigurationMetadata autoConfigurationMetadata) {
55+
AutoConfigurationMetadata autoConfigurationMetadata, UnaryOperator<String> replacementMapper) {
5256
Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null");
5357
this.metadataReaderFactory = metadataReaderFactory;
5458
this.autoConfigurationMetadata = autoConfigurationMetadata;
59+
this.replacementMapper = replacementMapper;
5560
}
5661

5762
List<String> getInPriorityOrder(Collection<String> classNames) {
@@ -108,7 +113,7 @@ private void checkForCycles(Set<String> processing, String current, String after
108113
() -> "AutoConfigure cycle detected between " + current + " and " + after);
109114
}
110115

111-
private static class AutoConfigurationClasses {
116+
private class AutoConfigurationClasses {
112117

113118
private final Map<String, AutoConfigurationClass> classes = new LinkedHashMap<>();
114119

@@ -157,7 +162,7 @@ Set<String> getClassesRequestedAfter(String className) {
157162

158163
}
159164

160-
private static class AutoConfigurationClass {
165+
private class AutoConfigurationClass {
161166

162167
private final String className;
163168

@@ -192,20 +197,36 @@ boolean isAvailable() {
192197

193198
Set<String> getBefore() {
194199
if (this.before == null) {
195-
this.before = (wasProcessed() ? this.autoConfigurationMetadata.getSet(this.className,
196-
"AutoConfigureBefore", Collections.emptySet()) : getAnnotationValue(AutoConfigureBefore.class));
200+
this.before = getClassNames("AutoConfigureBefore", AutoConfigureBefore.class);
197201
}
198202
return this.before;
199203
}
200204

201205
Set<String> getAfter() {
202206
if (this.after == null) {
203-
this.after = (wasProcessed() ? this.autoConfigurationMetadata.getSet(this.className,
204-
"AutoConfigureAfter", Collections.emptySet()) : getAnnotationValue(AutoConfigureAfter.class));
207+
this.after = getClassNames("AutoConfigureAfter", AutoConfigureAfter.class);
205208
}
206209
return this.after;
207210
}
208211

212+
private Set<String> getClassNames(String metadataKey, Class<? extends Annotation> annotation) {
213+
Set<String> annotationValue = wasProcessed()
214+
? this.autoConfigurationMetadata.getSet(this.className, metadataKey, Collections.emptySet())
215+
: getAnnotationValue(annotation);
216+
return applyReplacements(annotationValue);
217+
}
218+
219+
private Set<String> applyReplacements(Set<String> values) {
220+
if (AutoConfigurationSorter.this.replacementMapper == null) {
221+
return values;
222+
}
223+
Set<String> replaced = new LinkedHashSet<>(values);
224+
for (String value : values) {
225+
replaced.add(AutoConfigurationSorter.this.replacementMapper.apply(value));
226+
}
227+
return replaced;
228+
}
229+
209230
private int getOrder() {
210231
if (wasProcessed()) {
211232
return this.autoConfigurationMetadata.getInteger(this.className, "AutoConfigureOrder",

0 commit comments

Comments
 (0)