HUOXIU

Zhuanzhuan Recycling の強化: LiteFlow ビジュアルオーケストレーションソリューションの設計

出典: Zhuanzhuan Technology

  • 1 はじめに

  • 2. LiteFlowの紹介

    • 2.1 JARパッケージのインポート

    • 2.2 コンポーネントの定義

    • 2.3 実行プロセス

    • 2.4 公式サイト

  • 3. ビジュアルレイアウト(高度な形式)

    • 3.1 なぜ視覚化するのですか?

    • 3.2 スキーム設計

    • 3.3 プッシュプル複合リフレッシュプロセス

    • 3.4 ソースコード

  • 4. 結果と利点、そして今後の計画

    • 4.1 結果と利点

    • 4.2 将来計画

1 はじめに

LiteFlow はどのような問題を解決するのでしょうか? 以下の例を見てみましょう。

stepOne、stepTwo、stepThreeという3つのコンポーネント(またはメソッド)があり、「one」、「two」、「three」を順番に出力したいとします。通常、次のようなコードを記述します。

 @Component
public class PrintService {
/**
* 执行业务代码
*/

private void doExecute () {
stepOne();
stepTwo();
stepThree();
}

private void stepOne () {
// 业务代码
System.out.println( "one" );
}

private void stepTwo () {
// 业务代码
System.out.println( "two" );
}

private void stepThree () {
// 业务代码
System.out.println( "three" );
}
}

これは最も単純かつ直接的な書き方ですが、後で印刷順序を調整する必要がある場合、たとえば「2、1、3」を印刷したり、「2」をスキップして「1、3」を直接印刷したりする場合、コードを修正して再デプロイする必要があります。

 // 打印two、one、three
public void doExecute () {
stepTwo();
stepOne();
stepThree();
}
// 打印one、three
public void doExecute () {
stepOne();
stepThree();
}

実行プロセスの動的な調整が必要なビジネス シナリオの場合、プロセスをコード内にハードコードすることは明らかに適切ではありません。

2. LiteFlowの紹介

LiteFlow は、式を使用してコンポーネントまたはメソッドの実行フローをオーケストレーションできるオーケストレーション ベースのルール エンジン フレームワークであり、いくつかの高度なオーケストレーション手法もサポートしています。

上記のケースをより高度な方法で実装し、コード変更や再デプロイメントなしでプロセスをオーケストレーションするにはどうすればよいでしょうか。LiteFlow をベースにいくつか変更を加えました。

2.1 JARパッケージのインポート

公式ウェブサイトにアクセスして、ニーズに合わせて適切なバージョンをお選びください。ここでは最新バージョンを使用しています。

 < dependency >
< groupId > com.yomahub </ groupId >
< artifactId > liteflow-spring-boot-starter </ artifactId >
< version > 2.12.0 </ version >
</ dependency >

2.2 コンポーネントの定義

抽象親クラス NodeComponent から継承し、そのメソッドを実装して、印刷機能を個別のコンポーネントとして定義します。

 @Component
public class PrintOne extends NodeComponent {

@Override
public void process () throws Exception {
// 业务代码
System.out.println( "one" );
}
}

 @Component
public class PrintTwo extends NodeComponent {

@Override
public void process () throws Exception {
// 业务代码
System.out.println( "two" );
}
}

 @Component
public class PrintThree extends NodeComponent {

@Override
public void process () throws Exception {
// 业务代码
System.out.println( "three" );
}
}

2.3 実行プロセス

コンポーネントを定義したら、それらのコンポーネントの実行フロー式 (正式には EL 式と呼ばれます) の記述を開始できます。上記の例は次のように記述できます。

次に(ノード("printOne")、ノード("printTwo")、ノード("printThree"));

このプロセスに名前(プロセスの一意の識別子)を付けます: print_flow

プロセス名に基づいてプロセスを実行します。

 @Component
public class PrintService {

@Autowired
private FlowExecutor flowExecutor;

/**
* 执行业务代码
*/

public void doExecute () {
// 开始执行流程
LiteflowResponse response = flowExecutor.execute2Resp( "print_flow" );
// 根据执行结果进行后续操作
// ......
}
}

通常、このプロセスはデータベースに保存されます。印刷順序を変更したい場合は、式を変更するだけで済みます。例:print two, one, three。

次に(ノード("printTwo")、ノード("printOne")、ノード("pirntThree"));

「2」と「3」を出力します。

次に(ノード("printTwo")、ノード("printThree"));

しかし、LiteFlowの真の力はそれだけではありません。単純なシリアルオーケストレーションだけでなく、 WHEN (並列オーケストレーション)、 IF (条件オーケストレーション)、 SWITCH (選択オーケストレーション)、 FOR (循環オーケストレーション)といった、より複雑なロジックもサポートします。

2.4 公式サイト

上記のシンプルな例は、LiteFlowフレームワークに馴染みのない方のために基本的な理解を提供することを目的としています。LiteFlowに基づくビジネスプロセスを真に実装し、実際のビジネスシナリオに適用するには、公式ドキュメントを通じてフレームワークの動作原理とコアメカニズムをより深く理解する必要があります。
https://liteflow.cc/

3. ビジュアルレイアウト(高度な形式)

3.1 なぜ視覚化するのですか?

公式サイトでは、表現を修正する方法として手書きしか提供されていません。視覚的なツールがないため、手書きには以下のような多くの問題や不便さが生じる可能性があります。

  • エラーが発生しやすい: 式には 1 文字でも欠落したり、カンマが 1 つも欠落したりしてはいけません。
  • プロセスは目に見えません。これらのプロセスを思いつくには、私たちは頭脳に頼るしかありません。運用チームや製品チームがプロセスを理解したり議論したりしたい場合、他の描画ツールを使って手作業で描画し、表現するしかありません。
  • ノードは構成できません: 運用チームはさまざまなシナリオに応じてノードを動的に構成しますが、視覚的なインターフェースがないため、運用チームのメンバーは構成を変更する方法がありません。

したがって、プロセスオーケストレーションにおいて可視化は非常に重要です。これにより、研究開発部門はプロセスをより正確に理解・設計できるようになり、運用部門はプロセスをより効率的に監視・管理できるようになります。

3.2 スキーム設計

オンラインでオープンソースプロジェクトはいくつか存在しますが、それらはほとんどが個人によってメンテナンスされており、複雑なプロセスをうまく処理できず、品質もばらつきがあります。そこで、私は独自の調査と設計を行い、通常のノード、条件付きノード、選択ノード、並列ノードをサポートしています。ループノードは現在私の業務では必要ありませんが、必要な場合は自分で機能を拡張できます。ソリューションを理解すれば、ノードタイプの拡張は非常に簡単です。ビジュアルオーケストレーションを完成させるには、次の2つの問題を解決する必要があります。

  • ユーザーインタラクション用のフロントエンドキャンバス (LogicFlow が推奨されますが、他の使い慣れたものでもかまいません)。
  • キャンバス データを EL 式に変換します (DFS 再帰に基づく手書きアルゴリズム)。

このセクションでは、キャンバス データを EL に変換するプロセスに焦点を当てます。

3.2.1 全体的なプロセス

作成プロセス

プロセスの核心はステップ 5 にあり、これについては以下で詳しく説明します。

エコープロセス

EL の解析には多大なコストがかかるため、式を解析せず、フロントエンドから渡されたキャンバス JSON データをフロントエンドに直接返して表示することにしました。

3.2.2 バックエンドの抽象構文木設計

ノードタイプの列挙

 public enum NodeEnum {
// 普通节点,对应普通组件
COMMON,
// 并行节点,对应并行组件
WHEN,
// 判断节点,对应判断组件
IF,
// 选择节点,对应选择组件
SWITCH,
// 汇总节点(自定义)
SUMMARY,
// 开始节点(自定义)
START,
// 结束节点(自定义)
END;
}

一般

通常のノードの入次数と出次数は 1 です。

もし

評価されるノードには、入次数が 1、出次数が 2 の 1 つの真のブランチと 1 つの偽のブランチが含まれます。

スイッチ

後続の処理は、SWITCHによって返されるタグに基づいて決定されます。入次数は1、出次数は1より大きいです。

いつ

公式サイトには WHEN ノードの概念がないため、多くの問題を回避するためにここでカスタム WHEN ノードを定義します。

WHEN ノードを定義する理由は何ですか?

IF や SWITCH とは異なり、WHEN は出力次数が 1 より大きいノードであるため、プロセスを駆動する先行ノードがありません。

このようなプロセスを検討する場合、WHEN ノードのサポートがなければ、キャンバス上の表示は非常に貧弱なものになります。

 THEN(
IF(node( "c" ),
WHEN(
node( "a" ),
node( "b" ),
node( "d" ),
node( "e" )
).ignoreError( true )
),
node( "f" )
)

まとめ

このノードは公式サイトでは利用できません。これは、WHEN、IF、SWITCHノードなど、すべての分岐ノードを要約するために使用されるカスタムノードです。入力次数は1より大きく、出力次数は1です。

なぜSUMMARYノードを定義するのですか?

ELアルゴリズムは、深さ優先探索(DFS)アルゴリズムを参照しながら再帰的に構築されます。このネストされたアプローチは、終了マーカーなしで無期限に実行され続けます。

例えば:

図1に基づいてEL式を生成する

 THEN(
node( "c" ),
WHEN(
THEN(node( "b" ),node( "e" )),
THEN(node( "d" ),node( "e" ))
)
)

図2に基づいてEL式を生成する

 THEN(
node( "c" ),
WHEN(node( "b" ),node( "d" )),
node( "e" )
)

図 2 の EL 式が目的のものであることがわかります。

市場でよく知られているワークフローエンジンも、キャンバス上での要約処理をこのように設計しています。例えば、Activitiで並列ゲートウェイを使用して複数署名の会議を開始する場合、複数署名の会議終了時に結果を要約する際にも並列ゲートウェイを使用する必要があります。そうしないと、承認の重複が発生します。

始める

プロセスには、入次数が 0、出次数が 1 の開始ノードが必要です。
終わり

終了ノード: すべてのプロセスには、入次数が 1 で出次数が 0 の終了ノードが必要です。
上記のノードタイプのクラス定義

 // 抽象父类
@Getter
public abstract class Node {

// node的唯一id
private final String id;

// node名称,对应LiteFlow的Bean名称
private final String name;

// 入度
private final List<Node> pre = Lists.newArrayList();

// 节点类型
private final NodeEnum nodeEnum;

// 出度
private final List<Node> next = Lists.newArrayList();

protected Node (String id, String name, NodeEnum nodeEnum) {
this .id = id;
this .name = name;
this .nodeEnum = nodeEnum;
}

public void addNextNode (Node node) {
next.add(node);
}

public void addPreNode (Node preNode) {
pre.add(preNode);
}
}
// 普通节点
public class CommonNode extends Node {

public CommonNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.COMMON);
}
}
// 并行节点
public class WhenNode extends Node {

public WhenNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.WHEN);
}
}
// 判断节点
@Getter
public class IfNode extends Node {

private Node trueNode;

private Node falseNode;

public IfNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.IF);
}

public void setTrueNode (Node trueNode) {
this .trueNode = trueNode;
super .addNextNode(trueNode);
}

public void setFalseNode (Node falseNode) {
this .falseNode = falseNode;
super .addNextNode(falseNode);
}
}
// 选择节点
@Getter
public class SwitchNode extends Node {

private final Map<Node, String> nodeTagMap = Maps.newHashMap();

public SwitchNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.SWITCH);
}

public void putNodeTag (Node node, String tag) {
nodeTagMap.put(node, tag);
super .addNextNode(node);
}
}
// 开始节点
public class StartNode extends Node {

public StartNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.START);
}
}
// 结束节点
public class EndNode extends Node {

public EndNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.END);
}
}
// 汇总节点
public class SummaryNode extends Node {

public SummaryNode (@NonNull String id, @NonNull String name) {
super (id, name, NodeEnum.SUMMARY);
}
}

3.2.3 Canvas JSONデータ設計

キャンバス データは最終的に次のデータ構造を持つ JSON 構文ツリーで表現されます。

 {
"nodeEntities" : [
{
"id" : "节点的唯一id,由前端生成。必填" ,
"name" : "节点名称,对应LiteFlow的节点名称,spring的beanName。必填" ,
"label" : "前端节点展示名称,到时候给前端。必填" ,
"nodeType" : "节点的类型,有COMMON、IF、SWITCH、WHEN、START、END和SUMMARY。必填" ,
"x" : "x坐标。必填" ,
"y" : "y坐标。必填"
}
],
"nodeEdges" : [
{
"source" : "源节点。必填" ,
"target" : "目标节点。必填" ,
"ifNodeFlag" : "if类型节点的true和false,只有ifNode时必填,其他node随意" ,
"tag" : "switch类型的下层节点的tag,主机有switchNode时必填,其他node随意"
}
]
}

ユーザーがノードをドラッグ アンド ドロップして接続するプロセスは、実際にはノード配列とエッジ配列を維持するプロセスです。

3.2.4 Canvas JSONデータ検証

以下はキャンバスJSONデータの簡単な検証ルールです。必要に応じて拡張できます。実装は非常にシンプルです。具体的な実装コードは最後に記載しています。必要に応じてダウンロードできます。

  • プロセスには開始ノードと終了ノードが必要です。
  • 検証ノード タイプは、IF、WHEN、COMMON、SWITCH、START、END、SUMMARY のみとなります。
  • IF、WHEN、SWITCH ノードの合計数を、SUMMARY タイプのノードの合計数と比較します。
  • ノードとエッジのソースとターゲットが対応しているかどうかを確認します。
  • スイッチの出力次数エッジにタグがあるかどうかを確認します。タグは空にできません。
  • IF ノードに ifNodeFlag フラグがあるかどうかを確認し、常に 1 つの true ブランチと 1 つの false ブランチがあることを確認します。

3.2.5 Canvas JSONデータを抽象構文木に変換する

簡単な例を次に示します。対応する JSON 構文ツリーは次のようになります。長くなりすぎないように、ここでは一部の属性のみをリストします。

 {
"nodeEntities" : [
{
"id" : "a" ,
"label" : "a" ,
"nodeType" : "COMMON"
},
{
"id" : "e" ,
"label" : "e" ,
"nodeType" : "WHEN"
},
{
"id" : "b" ,
"label" : "b" ,
"nodeType" : "COMMON"
},
......
],
"nodeEdges" : [
{
"source" : "a" ,
"target" : "e" ,
},
{
"source" : "e" ,
"target" : "b" ,
},
{
"source" : "e" ,
"target" : "c" ,
},
......
]
}

JSONを抽象構文木に変換するには、基本的にノードオブジェクトを作成し、そのプロパティを維持する必要があります。以下は擬似コードです。

 // 创建节点对象
List<Node> nodes = Lists.newArrayList();
for (NodeEntity nodeEntity : nodeEntities) {
Node node = null ;
switch (nodeEntity.getNodeType()) {
case NodeEnum.COMMON;
node = new CommonNode( "节点的id" , "节点的label" );
break ;
case NodeEnum.WHEN;
node = new WhenNode( "节点的id" , "节点的label" );
break ;
case NodeEnum.SUMMARY;
node = new SummaryNode( "节点的id" , "节点的label" );
break ;
default :
throw new RuntimeException( "未知的节点类型!" );
}
nodes.add(node);
}
// 构建nodeId和node的map
Map<String, Node> nodeIdAndNodeMap = nodes.stream()
.collect(Collectors.toMap(Node::getId, Function.identity()));
// 维护节点间关系
for (NodeEdge nodeEdge : nodeEdges) {
Node sourceNode = nodeIdAndNodeMap.get(nodeEdge.getSource());
Node targetNode = nodeIdAndNodeMap.get(nodeEdge.getTarget());
sourceNode.addNextNode(targetNode);
targetNode.addPreNode(sourceNode);
......
}

質問: JSON と AST (抽象構文ツリー) という 2 つのデータ構造を設計するのはなぜですか?

上記のJSONデータに基づくと、ユーザーがキャンバスを編集する際、フロントエンドではノードとエッジの2つの配列のみを管理すればよく、EL式の生成処理はバックエンドで行われることがわかります。生成方法は、再帰的に実装された深さ優先探索(DFS アルゴリズムを使用しています。明らかにJSONは再帰要件を満たしていないため、JSONはASTに変換されます。

つまり、JSON と AST は、フロントエンドとバックエンドがそれぞれのデータを維持しやすくなるように設計されています。

3.2.6 抽象構文木からのEL式の生成

プロセス全体の核心はここにあります。AST は EL 式を生成します。

上記と同じ例を用いてEL式の生成プロセスをシミュレートすると、このプロセスにはTHENとWHENのみが含まれます。THENとWHENは配列として扱うことにします。例えば、 THEN(node("a"),node("b"))は配列[node("a"),node("b")]に対応し、WHENについても同様です。

  1. プロセスは配列から始まる必要があります。
 [
node( "a" )
]

  1. WHEN ブランチ ノード e に遭遇したら、新しい配列を作成し、それを親配列に追加します。
 [
node( "a" ),
[
]
]

  1. ブランチ ノードの後のブランチごとに配列を作成し、ブランチ ノードの配列に追加する必要があります。
 [
node( "a" ),
[
[
node( "b" )
]
]
]

  1. 通常のシリアル処理では、ノードは最も内側の配列に直接追加されます。
 [
node( "a" ),
[
[
node( "b" ),
node( "d" )
]
]
]

  1. サマリーノードに遭遇した場合は何も行いません。
 [
node( "a" ),
[
[
node( "b" ),
node( "d" )
]
]
]

  1. 下方向に進み、ノード WHEN を含む配列にノード f を追加して、再帰の終了ポイントに到達します。
 [
node( "a" ),
[
[
node( "b" ),
node( "d" )
]
],
node( "f" )
]

これにより、次のような疑問が生じるかもしれません。プログラムはどのようにして WHEN を含む配列を見つけるのでしょうか?

スタックを用いると、WHENノードに遭遇すると、そのWHENノードを含む配列がスタックにプッシュされます。サマリーノードに遭遇すると、その配列はスタックからポップされます。したがって、ノードfはスタックからポップされたときに配列に追加する必要があると判断できます。


  1. 分岐プロセスはノード e から開始されるため、ノード b から始まる分岐は完了しており、別の分岐に戻ります。同様に、ノード c は e の分岐であり、分岐ノードの後の各分岐は配列を作成し、それを分岐ノードの配列に追加する必要があります。
 [
node( "a" ),
[
[
node( "b" ),
node( "d" )
],
[
node( "c" )
]
],
node( "f" )
]

  1. サマリー ノードに到達すると、ノード b から始まるブランチをトラバースしたときに既にサマリー ノードにアクセスしているため、今回はサマリー ノードを処理せず、再帰の出口に到達します。
 [
node( "a" ),
[
[
node( "b" ),
node( "d" )
],
[
node( "c" )
]
],
node( "f" )
]

サマリーノードがアクセスされたかどうかを確認するにはどうすればよいですか?

Set を使用すると、訪問済みのサマリーノードが Set に追加されます。次回アクセスされる際には、まず Set がチェックされます。サマリーノードが見つかった場合、プロセスは停止し、再帰は終了します。

仕上げる!


上記の簡単な例に基づいて、 DFS を再帰的に実装するための疑似コードを以下に示します。完全なソース コードは記事の最後にあります。興味のある方は、参考のためにダウンロードできます。

 public static String ast2El (Node head) {
if (head == null ) {
return null ;
}
// 用于存放when节点List
Deque<List> stack = new ArrayDeque<>();
// 用于标记是否处理过summary节点了
Set<String> doneSummary = Sets.newHashSet();
List list = tree2El(head, new ArrayList(), stack, doneSummary);
// 将list生成EL,你可以认为框架有对应的方法
return toEL(list);
}

private static List tree2El (Node currentNode,
List currentThenList,
Deque<List> stack,
Set<String> doneSummary)
{
switch (currentNode.getNodeEnum()) {
case COMMON:
currentThenList.add(currentNode.getId());
for (Node nextNode : currentNode.getNext()) {
tree2El(nextNode, currentThenList, stack, doneSummary);
}
case WHEN:
stack.push(currentThenList);
List whenELList = new ArrayList<>();
currentThenList.add(whenELList);
for (Node nextNode : currentNode.getNext()) {
List thenELList = new ArrayList<>();
whenELList.add(thenELList);
tree2El(nextNode, thenELList , stack, doneSummary);
}
case SUMMARY:
if (!doneSummary.contains(currentNode.getId())) {
doneSummary.add(currentNode.getId());
// 这种节点只有0个或者1个nextNode
for (Node nextNode : currentNode.getNext()) {
tree2El(nextNode, stack.pop(), stack, doneSummary);
}
}
default :
throw new RuntimeException( "未知的节点类型!" );
}
return currentThenList;
}

3.2.7 EL式の妥当性の検証

これは EL 式を生成する最後のステップです。フレームワーク自体には、EL 式の生成後に実行される EL 式の検証をサポートするメソッドがあります。

 // 校验是否符合EL语法
Boolean isValid = LiteFlowChainELBuilder.validate(el);

最後のステップを完了すると、EL 式をデータベースに保存できます。

3.3 プッシュプル複合リフレッシュプロセス

このプロセスはデータベースへの入力後すぐには有効になりません。以下の手順を実行した後に有効になります。

3.3.1 プル

フレームワークは、データベース(または設定で指定された任意のデータソース)から最新のプロセスを定期的に同期し、メモリにキャッシュします。新しいプロセスの同期とキャッシュはスムーズに実行され、既存のプロセスの実行を妨げたり中断したりすることはありません。また、フレームワークでは、ユーザーが実際のニーズに応じてデータ更新間隔(デフォルトは1分)を設定することもできます。設定方法の詳細については、公式ドキュメントを参照してください。

3.3.2 プッシュ

フレームワークが受動的に更新されるのを待つのではなく、EL 式の変更をすぐに有効にしたい場合は、公式 API を使用してアクティブに更新できます。

 flowExecutor.reloadRule();

公式の方法で更新されるのは単一のインスタンスのノードのみであることに注意することが重要です。クラスター環境では、メッセージ キューを使用してクラスター全体に通知する必要があります。

3.4 ソースコード

この設計スキームは実際のビジネスシナリオに実装され、使用されています。多くの複雑なプロセスを検証し、このルールに基づいて生成されるEL式の正確性を100%保証できます。

これは私が自分で作成したデモです。そのアイデアを参考にしてください。事前に構築された複雑なプロセスの例が含まれており、APIを呼び出すことで実際に体験できます。

https://dl.zhuanstatic.com/fecommon/liteFlow-el.zip

4. 結果と利点、そして今後の計画

プロセスの視覚的なオーケストレーションを導入し、それを LiteFlow フレームワークのサポートと組み合わせることで、プロセス設計の直感性と開発効率が大幅に向上し、プロジェクトにスムーズで効率的な開発エクスペリエンスをもたらします。

4.1 結果と利点:

  1. 開発者は、構文ルールに時間をかけすぎずに、コアビジネスプロセスの設計に集中できます。
  2. 直感的なビジュアルプロセスインターフェースにより、製品チームと研究開発チーム間のコミュニケーションがより効率的になり、複雑なビジネスロジックを明確に表示でき、不要なコミュニケーションを回避できます。
  3. 運用担当者はプロセス ノードをリアルタイムで編集し、ノードの属性構成(たとえば、「ブラックリスト検証」ノードに設定されているユーザーなど)を迅速に把握できるため、ビジネス プロセスをより柔軟に管理できます。

4.2 将来計画

問題点:

  1. プロセス オーケストレーションは既存のノードにのみ適用されます。新しいビジネス ノードにはさらなる開発が必要です。
  2. 外部の当事者にサービスを提供する場合、呼び出し側が比較的詳細なパラメータ情報を提供することが必要になる場合があります。

企画

  1. 将来的には、動的スクリプトを活用して、開発作業を必要とせずにまったく新しいビジネス プロセスを迅速に構築することが目標です。
  2. データディクショナリの概念を導入することで、よく使用されるパラメータをデータディクショナリに統合できます。例えば、注文番号を入力するだけで、処理に必要なパラメータをデータディクショナリから取得できるため、呼び出し側の開発コストを削減できます。

著者について

江涛氏、バックエンドエンジニア、技術部門、Zhuanzhuan Recycling