Designing Visual Workflow Orchestration with LiteFlow: From Basic Components to Advanced AST Generation
This article explains how to use the LiteFlow rule‑engine framework to define reusable Java components, express execution flows with EL expressions, design a visual canvas that converts JSON into an abstract syntax tree, generate valid EL code via DFS, and manage workflow updates through push‑pull refresh mechanisms, while also outlining benefits and future plans.
LiteFlow is a rule‑engine framework that allows developers to compose component execution flows using EL expressions instead of hard‑coded method calls, enabling dynamic reordering without redeploying code.
Basic component definition shows three Spring beans (PrintOne, PrintTwo, PrintThree) extending NodeComponent and overriding process() to print "one", "two", and "three" respectively:
@Component
public class PrintOne extends NodeComponent {
@Override
public void process() throws Exception {
System.out.println("one");
}
}The execution flow is expressed as an EL string, e.g. THEN(node("printOne"),node("printTwo"),node("printThree")) , stored in the database under the identifier print_flow . The flow is triggered via flowExecutor.execute2Resp("print_flow") .
Visual orchestration addresses the difficulty of manually editing EL expressions by providing a front‑end canvas (e.g., LogicFlow) where users drag nodes and connect edges. The canvas data is serialized to JSON containing nodeEntities and nodeEdges , each describing node id, name, type, coordinates, and edge relationships.
The backend defines an enum NodeEnum with values COMMON, WHEN, IF, SWITCH, SUMMARY, START, END, and implements concrete node classes (CommonNode, WhenNode, IfNode, SwitchNode, SummaryNode, StartNode, EndNode) that extend an abstract Node class holding id, name, type, predecessor and successor lists.
public enum NodeEnum {
COMMON, WHEN, IF, SWITCH, SUMMARY, START, END;
}Conversion from the canvas JSON to an abstract syntax tree (AST) is performed by creating node objects based on nodeType , building a map from id to node, and linking nodes according to nodeEdges . Pseudocode:
List
nodes = new ArrayList<>();
for (NodeEntity ne : nodeEntities) {
Node node = null;
switch (ne.getNodeType()) {
case COMMON: node = new CommonNode(ne.getId(), ne.getLabel()); break;
case WHEN: node = new WhenNode(ne.getId(), ne.getLabel()); break;
case SUMMARY: node = new SummaryNode(ne.getId(), ne.getLabel()); break;
// other cases …
default: throw new RuntimeException("Unknown node type!");
}
nodes.add(node);
}
Map
idMap = nodes.stream().collect(Collectors.toMap(Node::getId, Function.identity()));
for (NodeEdge edge : nodeEdges) {
Node src = idMap.get(edge.getSource());
Node tgt = idMap.get(edge.getTarget());
src.addNextNode(tgt);
tgt.addPreNode(src);
}Generating the EL expression from the AST uses a depth‑first search (DFS) algorithm with a stack to handle WHEN branches and a set to avoid processing SUMMARY nodes multiple times. The core method ast2El(Node head) returns the final EL string, which is then validated by LiteFlowChainELBuilder.validate(el) .
public static String ast2El(Node head) {
if (head == null) return null;
Deque
stack = new ArrayDeque<>();
Set
doneSummary = new HashSet<>();
List list = tree2El(head, new ArrayList(), stack, doneSummary);
return toEL(list);
}
private static List tree2El(Node cur, List curList, Deque
stack, Set
done) {
switch (cur.getNodeEnum()) {
case COMMON:
curList.add(cur.getId());
for (Node n : cur.getNext()) tree2El(n, curList, stack, done);
break;
case WHEN:
stack.push(curList);
List whenList = new ArrayList();
curList.add(whenList);
for (Node n : cur.getNext()) {
List thenList = new ArrayList();
whenList.add(thenList);
tree2El(n, thenList, stack, done);
}
break;
case SUMMARY:
if (!done.contains(cur.getId())) {
done.add(cur.getId());
for (Node n : cur.getNext()) tree2El(n, stack.pop(), stack, done);
}
break;
default:
throw new RuntimeException("Unknown node type!");
}
return curList;
}After validation, the EL expression can be stored in the database. The framework supports a pull‑based refresh (periodic sync from DB) and a push‑based refresh via flowExecutor.reloadRule(); . In clustered deployments, a message queue is required to broadcast the refresh.
The article concludes with observed benefits—reduced code changes, clearer communication between product and engineering, and operability for non‑technical staff—and future directions such as dynamic scripting and data‑dictionary‑driven parameters.
Zhuanzhuan Tech
A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.