8 changed files with 566 additions and 4 deletions
@ -0,0 +1,49 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -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 {}; |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -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 {}; |
||||
} |
||||
@ -0,0 +1,153 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,120 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,80 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue