From a0c3aecc1afac4e6575f5db0333d8fd0dd597823 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Sat, 26 Sep 2026 21:15:32 +0300 Subject: [PATCH 1/2] Improve auto-configuration sorting performance Update `AutoConfigurationSorter` to improve performance by building reverse `@AutoConfigureBefore` relationships while the auto-configuration graph is created and using stable sort ranks during traversal. See gh-51867 Signed-off-by: aalsanie --- .../AutoConfigurationSorter.java | 47 +++++++++++-------- .../AutoConfigurationSorterTests.java | 32 +++++++++++++ 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index f44b05573b2..bd6054c6f44 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -82,29 +82,34 @@ class AutoConfigurationSorter { } private List sortByAnnotation(AutoConfigurationClasses classes, List classNames) { - List toSort = new ArrayList<>(classNames); - toSort.addAll(classes.getAllNames()); + Set requestedClassNames = new LinkedHashSet<>(classNames); + Map sortOrder = new LinkedHashMap<>(); + for (String className : classNames) { + sortOrder.putIfAbsent(className, sortOrder.size()); + } + for (String className : classes.getAllNames()) { + sortOrder.putIfAbsent(className, sortOrder.size()); + } Set sorted = new LinkedHashSet<>(); Set processing = new LinkedHashSet<>(); - while (!toSort.isEmpty()) { - doSortByAfterAnnotation(classes, toSort, sorted, processing, null); + for (String className : sortOrder.keySet()) { + if (!sorted.contains(className)) { + doSortByAfterAnnotation(classes, sortOrder, sorted, processing, className); + } } - sorted.retainAll(classNames); + sorted.retainAll(requestedClassNames); return new ArrayList<>(sorted); } - private void doSortByAfterAnnotation(AutoConfigurationClasses classes, List toSort, Set sorted, - Set processing, @Nullable String current) { - if (current == null) { - current = toSort.remove(0); - } + private void doSortByAfterAnnotation(AutoConfigurationClasses classes, Map sortOrder, + Set sorted, Set processing, String current) { processing.add(current); - Set afters = new TreeSet<>(Comparator.comparing(toSort::indexOf)); + Set afters = new TreeSet<>(Comparator.comparingInt((String name) -> sortOrder.getOrDefault(name, -1))); afters.addAll(classes.getClassesRequestedAfter(current)); for (String after : afters) { checkForCycles(processing, current, after); - if (!sorted.contains(after) && toSort.contains(after)) { - doSortByAfterAnnotation(classes, toSort, sorted, processing, after); + if (!sorted.contains(after) && sortOrder.containsKey(after)) { + doSortByAfterAnnotation(classes, sortOrder, sorted, processing, after); } } processing.remove(current); @@ -120,6 +125,8 @@ class AutoConfigurationSorter { private final Map classes = new LinkedHashMap<>(); + private final Map> classesRequestedAfter = new LinkedHashMap<>(); + AutoConfigurationClasses(MetadataReaderFactory metadataReaderFactory, @Nullable AutoConfigurationMetadata autoConfigurationMetadata, Collection classNames) { addToClasses(metadataReaderFactory, autoConfigurationMetadata, classNames, true); @@ -141,8 +148,12 @@ class AutoConfigurationSorter { this.classes.put(className, autoConfigurationClass); } if (available) { - addToClasses(metadataReaderFactory, autoConfigurationMetadata, - autoConfigurationClass.getBefore(), false); + Set before = autoConfigurationClass.getBefore(); + for (String name : before) { + this.classesRequestedAfter.computeIfAbsent(name, (key) -> new LinkedHashSet<>()) + .add(className); + } + addToClasses(metadataReaderFactory, autoConfigurationMetadata, before, false); addToClasses(metadataReaderFactory, autoConfigurationMetadata, autoConfigurationClass.getAfter(), false); } @@ -158,11 +169,7 @@ class AutoConfigurationSorter { Set getClassesRequestedAfter(String className) { Set classesRequestedAfter = new LinkedHashSet<>(get(className).getAfter()); - this.classes.forEach((name, autoConfigurationClass) -> { - if (autoConfigurationClass.getBefore().contains(className)) { - classesRequestedAfter.add(name); - } - }); + classesRequestedAfter.addAll(this.classesRequestedAfter.getOrDefault(className, Collections.emptySet())); return classesRequestedAfter; } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java index fa6076dd703..ac47153e6ed 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationSorterTests.java @@ -188,6 +188,38 @@ class AutoConfigurationSorterTests { assertThat(actual).containsExactly(B, A); } + @Test + void orderIsStableForLargerMixedGraph() { + List actual = getInPriorityOrder(LOWEST, HIGHEST, DEFAULT, A, A2, B, C, E, W, W2, X, Y, Z); + assertThat(actual).containsExactly(HIGHEST, C, E, W, W2, B, A, A2, Z, Y, X, DEFAULT, LOWEST); + } + + @Test + void orderIsStableForLargerMixedGraphWithReversedInput() { + List actual = getInPriorityOrder(Z, Y, X, W2, W, E, C, B, A2, A, DEFAULT, HIGHEST, LOWEST); + assertThat(actual).containsExactly(HIGHEST, C, E, W, W2, B, A, A2, Z, Y, X, DEFAULT, LOWEST); + } + + @Test + void duplicateInputDoesNotChangeResult() { + List actual = getInPriorityOrder(A, B, C, B, A, C); + assertThat(actual).containsExactly(C, B, A); + } + + @Test + void transitivelyDiscoveredClassesAreNotIncludedInResult() { + List actual = getInPriorityOrder(A, C); + assertThat(actual).containsExactly(C, A); + } + + @Test + void cycleReportsSameClasses() { + this.sorter = new AutoConfigurationSorter(new CachingMetadataReaderFactory(), this.autoConfigurationMetadata, + REPLACEMENT_MAPPER); + assertThatIllegalStateException().isThrownBy(() -> getInPriorityOrder(A, B, C, D)) + .withMessage("AutoConfigure cycle detected between " + D + " and " + A); + } + @Test void byAutoConfigureAfterWithCycle() { this.sorter = new AutoConfigurationSorter(new CachingMetadataReaderFactory(), this.autoConfigurationMetadata, From 875efab7d52afea392a2680ab9f2bb785697c05f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 28 Sep 2026 15:33:09 -0700 Subject: [PATCH 2/2] Polish 'Improve auto-configuration sorting performance' See gh-51867 --- .../AutoConfigurationSorter.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index bd6054c6f44..6de03997498 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -77,38 +77,34 @@ class AutoConfigurationSorter { return Integer.compare(i1, i2); }); // Then respect @AutoConfigureBefore @AutoConfigureAfter - orderedClassNames = sortByAnnotation(classes, orderedClassNames); + orderedClassNames = sortByAnnotation(new LinkedHashSet<>(orderedClassNames), classes); return orderedClassNames; } - private List sortByAnnotation(AutoConfigurationClasses classes, List classNames) { - Set requestedClassNames = new LinkedHashSet<>(classNames); + private List sortByAnnotation(Set classNames, AutoConfigurationClasses classes) { Map sortOrder = new LinkedHashMap<>(); - for (String className : classNames) { - sortOrder.putIfAbsent(className, sortOrder.size()); - } - for (String className : classes.getAllNames()) { - sortOrder.putIfAbsent(className, sortOrder.size()); - } + classNames.forEach((className) -> sortOrder.putIfAbsent(className, sortOrder.size())); + classes.getAllNames().forEach((className) -> sortOrder.putIfAbsent(className, sortOrder.size())); Set sorted = new LinkedHashSet<>(); Set processing = new LinkedHashSet<>(); for (String className : sortOrder.keySet()) { - if (!sorted.contains(className)) { - doSortByAfterAnnotation(classes, sortOrder, sorted, processing, className); - } + doSortByAfterAnnotation(classes, sortOrder, sorted, processing, className); } - sorted.retainAll(requestedClassNames); + sorted.retainAll(classNames); return new ArrayList<>(sorted); } private void doSortByAfterAnnotation(AutoConfigurationClasses classes, Map sortOrder, Set sorted, Set processing, String current) { + if (sorted.contains(current)) { + return; + } processing.add(current); Set afters = new TreeSet<>(Comparator.comparingInt((String name) -> sortOrder.getOrDefault(name, -1))); afters.addAll(classes.getClassesRequestedAfter(current)); for (String after : afters) { checkForCycles(processing, current, after); - if (!sorted.contains(after) && sortOrder.containsKey(after)) { + if (sortOrder.containsKey(after)) { doSortByAfterAnnotation(classes, sortOrder, sorted, processing, after); } }