This commit is contained in:
Yongjun Hong 2025-10-07 23:10:35 +03:00 committed by GitHub
commit a9ed304ea0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 566 additions and 4 deletions

View File

@ -0,0 +1,49 @@
/*
* Copyright 2002-present 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.core;
import java.util.List;
import java.util.stream.Collectors;
/**
* Exception thrown when a cyclic ordering dependency is detected.
*
* @author Yongjun Hong
*/
@SuppressWarnings("serial")
public class CyclicOrderException extends RuntimeException {
private final List<Object> cycle;
public CyclicOrderException(String message, List<Object> cycle) {
super(buildDetailedMessage(message, cycle));
this.cycle = cycle;
}
private static String buildDetailedMessage(String message, List<Object> cycle) {
String cycleDescription = cycle.stream()
.map(obj -> (obj instanceof Class) ? ((Class<?>) obj).getSimpleName() : obj.getClass().getSimpleName())
.collect(Collectors.joining(" -> "));
return message + ". Detected cycle: " + cycleDescription + " -> " +
cycle.get(0).getClass().getSimpleName();
}
public List<Object> getCycle() {
return this.cycle;
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2002-present 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.core;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated component should be ordered after the specified target classes.
*
* <p>This annotation is an extension of the {@code @AutoConfigureAfter} pattern from Spring Boot,
* adapted for use in the core Spring framework. It allows developers to specify that a component
* must be initialized or processed after certain other components.</p>
*
* <p>For example, if class A depends on class B being initialized first, class A can be annotated
* with {@code @DependsOnAfter(B.class)} to enforce this order.</p>
*
* <p>This annotation is particularly useful in scenarios where the initialization order of
* components affects application behavior, and topological sorting is required to resolve
* dependencies.</p>
*
* @author Yongjun Hong
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DependsOnAfter {
/**
* The target classes after which this component should be ordered.
*/
Class<?>[] value() default {};
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-present 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.core;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated component should be ordered before the specified target classes.
*
* <p>This annotation extends the {@code @AutoConfigureBefore} pattern from Spring Boot
* to the core Spring framework, allowing for fine-grained control over the initialization
* or processing order of components.</p>
*
* <p>For example, if class A must be initialized before class B, you can annotate class A
* with {@code @DependsOnBefore(B.class)}.</p>
*
* <p>This annotation is primarily used in dependency management scenarios where
* topological sorting is required to determine the correct order of execution.</p>
*
* @author Yongjun Hong
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DependsOnBefore {
/**
* The target classes before which this component should be ordered.
*/
Class<?>[] value() default {};
}

View File

@ -0,0 +1,153 @@
/*
* Copyright 2002-present 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.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
/**
* A directed graph to represent relative ordering relationships.
*
* @author Yongjun Hong
*/
public class OrderGraph {
private final Map<Object, Set<Object>> adjacencyList = new HashMap<>();
private final Map<Object, Integer> inDegree = new HashMap<>();
private final Set<Object> allNodes = new HashSet<>();
public void addNode(Object node) {
this.allNodes.add(node);
this.adjacencyList.putIfAbsent(node, new HashSet<>());
this.inDegree.putIfAbsent(node, 0);
}
/**
* Adds an edge indicating that 'from' must be ordered before 'to'.
*/
public void addEdge(Object from, Object to) {
addNode(from);
addNode(to);
Set<Object> neighbors = this.adjacencyList.get(from);
if (neighbors != null && neighbors.add(to)) {
this.inDegree.put(to, this.inDegree.getOrDefault(to, 0) + 1);
}
}
/**
* Performs a topological sort using Kahn's algorithm.
*/
public List<Object> topologicalSort() {
Map<Object, Integer> tempInDegree = new HashMap<>(this.inDegree);
Queue<Object> queue = new LinkedList<>();
List<Object> result = new ArrayList<>();
for (Object node : this.allNodes) {
if (tempInDegree.getOrDefault(node, 0) == 0) {
queue.offer(node);
}
}
while (!queue.isEmpty()) {
Object current = queue.poll();
result.add(current);
for (Object neighbor : this.adjacencyList.getOrDefault(current, Collections.emptySet())) {
tempInDegree.put(neighbor, tempInDegree.getOrDefault(neighbor, 0) - 1);
if (tempInDegree.get(neighbor) == 0) {
queue.offer(neighbor);
}
}
}
if (result.size() != this.allNodes.size()) {
List<Object> cycle = detectCycle();
throw new CyclicOrderException("Circular ordering dependency detected", cycle);
}
return result;
}
/**
* Detects a cycle in the graph using Depth-First Search (DFS).
*/
public List<Object> detectCycle() {
Set<Object> visited = new HashSet<>();
Set<Object> recursionStack = new HashSet<>();
Map<Object, Object> parent = new HashMap<>();
for (Object node : this.allNodes) {
if (!visited.contains(node)) {
List<Object> cycle = dfsDetectCycle(node, visited, recursionStack, parent);
if (!cycle.isEmpty()) {
return cycle;
}
}
}
return Collections.emptyList();
}
private List<Object> dfsDetectCycle(Object node, Set<Object> visited,
Set<Object> recursionStack, Map<Object, Object> parent) {
visited.add(node);
recursionStack.add(node);
for (Object neighbor : this.adjacencyList.getOrDefault(node, Collections.emptySet())) {
if (neighbor != null) {
parent.put(neighbor, node);
}
if (!visited.contains(neighbor)) {
List<Object> cycle = dfsDetectCycle(neighbor, visited, recursionStack, parent);
if (!cycle.isEmpty()) {
return cycle;
}
}
else if (recursionStack.contains(neighbor)) {
// Cycle detected - build the cycle path for the exception message
return buildCyclePath(neighbor, node, parent);
}
}
recursionStack.remove(node);
return Collections.emptyList();
}
private List<Object> buildCyclePath(Object cycleStart, Object current, Map<Object, Object> parent) {
List<Object> cycle = new ArrayList<>();
Object node = current;
while (node != null && !node.equals(cycleStart)) {
cycle.add(node);
node = parent.get(node);
}
if (node != null) {
cycle.add(node);
}
Collections.reverse(cycle);
return cycle;
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2002-present 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.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
/**
* A processor that sorts a list of components based on @Order, @DependsOnBefore, and @DependsOnAfter annotations.
* This design separates the preparation phase (graph building and topological sort)
* from the comparison phase, adhering to the standard Comparator contract.
*
* @author Yongjun Hong
*/
public final class RelativeOrderProcessor {
/**
* hared default instance of {@code RelativeOrderProcessor}.
*/
public static final RelativeOrderProcessor INSTANCE = new RelativeOrderProcessor();
private static final TopologicalOrderSolver TOPOLOGICAL_ORDER_SOLVER = new TopologicalOrderSolver();
private RelativeOrderProcessor() {
// Private constructor to prevent instantiation
}
/**
* Sorts the given list of objects in place.
* @param list the list to be sorted.
*/
public static void sort(List<?> list) {
if (list.size() <= 1) {
return;
}
list.sort(getComparatorFor(list));
}
/**
* Creates a comparator tailored to the specific components in the input list.
* It pre-calculates the topological order for components with relative ordering needs.
* @param items the collection of items to create a comparator for.
* @return a fully configured Comparator.
*/
private static Comparator<Object> getComparatorFor(Collection<?> items) {
List<Object> components = new ArrayList<>(items);
Map<Object, Integer> orderMap = new HashMap<>();
if (!components.isEmpty()) {
List<Object> sortedRelative = TOPOLOGICAL_ORDER_SOLVER.resolveOrder(components);
for (int i = 0; i < sortedRelative.size(); i++) {
orderMap.put(sortedRelative.get(i), i);
}
}
return new ConfiguredComparator(orderMap);
}
private static boolean hasRelativeOrderAnnotations(Object obj) {
if (obj == null) {
return false;
}
Class<?> clazz = (obj instanceof Class) ? (Class<?>) obj : obj.getClass();
return AnnotationUtils.findAnnotation(clazz, DependsOnBefore.class) != null ||
AnnotationUtils.findAnnotation(clazz, DependsOnAfter.class) != null;
}
/**
* The actual comparator implementation. It uses a pre-computed order map for relative
* components and falls back to AnnotationAwareOrderComparator for everything else.
*/
private static class ConfiguredComparator implements Comparator<Object> {
private final Map<Object, Integer> orderMap;
private final Comparator<Object> fallbackComparator = AnnotationAwareOrderComparator.INSTANCE;
public ConfiguredComparator(Map<Object, Integer> orderMap) {
this.orderMap = orderMap;
}
@Override
public int compare(Object o1, Object o2) {
boolean o1isRelative = hasRelativeOrderAnnotations(o1);
boolean o2isRelative = hasRelativeOrderAnnotations(o2);
// Case 1: Either components have relative order. Compare their topological index.
if (o1isRelative || o2isRelative) {
int order1 = this.orderMap.getOrDefault(o1, 0);
int order2 = this.orderMap.getOrDefault(o2, 0);
return Integer.compare(order1, order2);
}
// Case 2: One is relative, the other is absolute, or both are absolute.
// Use the fallback comparator (@Order, @Priority) for tie-breaking.
return this.fallbackComparator.compare(o1, o2);
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2002-present 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.core;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.annotation.AnnotationUtils;
/**
* Resolves relative ordering relationships using a topological sort.
*
* @author Yongjun Hong
*/
public class TopologicalOrderSolver {
/**
* Resolves the relative order of the given components and returns a sorted list.
*/
public List<Object> resolveOrder(List<Object> components) {
if (components.size() <= 1) {
return new ArrayList<>(components);
}
OrderGraph graph = buildOrderGraph(components);
return graph.topologicalSort();
}
/**
* Builds the ordering relationship graph from the given components.
*/
private OrderGraph buildOrderGraph(List<Object> components) {
OrderGraph graph = new OrderGraph();
for (Object component : components) {
graph.addNode(component);
}
for (Object component : components) {
addOrderConstraints(graph, component);
}
return graph;
}
private void addOrderConstraints(OrderGraph graph, Object component) {
if (component == null) {
return;
}
Class<?> componentClass = (component instanceof Class) ? (Class<?>) component : component.getClass();
// Process @DependsOnBefore
DependsOnBefore dependsOnBefore = AnnotationUtils.findAnnotation(componentClass, DependsOnBefore.class);
if (dependsOnBefore != null) {
for (Class<?> beforeClass : dependsOnBefore.value()) {
graph.addEdge(component, beforeClass);
}
}
// Process @DependsOnAfter
DependsOnAfter dependsOnAfter = AnnotationUtils.findAnnotation(componentClass, DependsOnAfter.class);
if (dependsOnAfter != null) {
for (Class<?> afterClass : dependsOnAfter.value()) {
graph.addEdge(afterClass, component);
}
}
}
}

View File

@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.OrderComparator;
import org.springframework.core.RelativeOrderProcessor;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
/**
@ -40,6 +41,7 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
* @author Juergen Hoeller
* @author Oliver Gierke
* @author Stephane Nicoll
* @author Yongjun Hong
* @since 2.0.1
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.Order
@ -105,9 +107,10 @@ public class AnnotationAwareOrderComparator extends OrderComparator {
* @see java.util.List#sort(java.util.Comparator)
*/
public static void sort(List<?> list) {
if (list.size() > 1) {
list.sort(INSTANCE);
if (list.size() <= 1) {
return;
}
RelativeOrderProcessor.sort(list);
}
/**
@ -118,9 +121,10 @@ public class AnnotationAwareOrderComparator extends OrderComparator {
* @see java.util.Arrays#sort(Object[], java.util.Comparator)
*/
public static void sort(Object[] array) {
if (array.length > 1) {
Arrays.sort(array, INSTANCE);
if (array.length <= 1) {
return;
}
RelativeOrderProcessor.sort(Arrays.asList(array));
}
/**

View File

@ -22,11 +22,17 @@ import java.util.List;
import jakarta.annotation.Priority;
import org.junit.jupiter.api.Test;
import org.springframework.core.CyclicOrderException;
import org.springframework.core.DependsOnAfter;
import org.springframework.core.DependsOnBefore;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Juergen Hoeller
* @author Oliver Gierke
* @author Yongjun Hong
*/
class AnnotationAwareOrderComparatorTests {
@ -100,6 +106,44 @@ class AnnotationAwareOrderComparatorTests {
assertThat(list).containsExactly(A.class, B.class, null, null);
}
@Test
void sortWithDependsOnBefore() {
List<Object> list = new ArrayList<>();
list.add(A.class);
list.add(D.class);
AnnotationAwareOrderComparator.sort(list);
assertThat(list).containsExactly(D.class, A.class);
}
@Test
void sortWithDependsOnAfter() {
List<Object> list = new ArrayList<>();
list.add(B.class);
list.add(E.class);
AnnotationAwareOrderComparator.sort(list);
assertThat(list).containsExactly(B.class, E.class);
}
@Test
void sortWithDependsOnBeforeAndAfter() {
List<Object> list = new ArrayList<>();
list.add(A.class);
list.add(B.class);
list.add(D.class);
list.add(E.class);
AnnotationAwareOrderComparator.sort(list);
assertThat(list).containsExactly(D.class, A.class, B.class, E.class);
}
@Test
void sortWithCircularDependsOn() {
List<Object> list = new ArrayList<>();
list.add(F.class);
list.add(G.class);
assertThatThrownBy(() -> AnnotationAwareOrderComparator.sort(list))
.isInstanceOf(CyclicOrderException.class);
}
@Order(1)
private static class A {
}
@ -119,4 +163,19 @@ class AnnotationAwareOrderComparatorTests {
private static class B2 {
}
@DependsOnBefore(A.class)
private static class D {
}
@DependsOnAfter(B.class)
private static class E {
}
@DependsOnBefore(G.class)
private static class F {
}
@DependsOnBefore(F.class)
private static class G {
}
}