軽量ワークフロー エンジンの設計と実装

1.ワークフローエンジンとは

ワークフロー エンジンは、ワークフローの実行を駆動する一連のコードです。

ワークフローとは何か、ワークフローがある理由、ワークフローの適用シナリオについては、学生はインターネット上の情報を参照できますが、ここでは詳しく説明しません。

 

2. ホイールを繰り返す理由

Activiti、flowable、Camundaなど、オープンソースのワークフローエンジンはたくさんあるので、そちらを選んでみてはいかがでしょうか。以下の考慮事項に基づきます。

  • 最も重要なことは、ビジネス ニーズを満たすことができず、いくつかの特別なシナリオを実現できないことです。
  • 一部の要件は実装がより複雑であり、さらに、エンジン データベースを直接変更する必要があるため、エンジンの安定した動作に大きな危険が潜んでおり、エンジンの将来のバージョン アップグレードにもいくつかの困難が生じます。
  • 資料、コード量、APIが多く、学習コストが高く、保守性が悪い。
  • 分析と評価の後、ビジネス シナリオに必要な BPMN 要素は少なくなり、開発と実装のコストは高くありません。

したがって、車輪の再発明には、実際には、より深い戦略的考慮事項があります。つまり、テクノロジー企業として、私たちは独自のコアとなる基盤テクノロジーを持たなければなりません! このように、人が制御することはできません (最近のチップの問題を参照してください)。

 

3. 車輪の作り方

学びの共有は、結果よりも過程が重要で、結果だけを語って過程を語らず、詳細を語らないものは、筋肉のショーであり、真の共有ではないと思います。ということで、今回はホイールの主な工程を中心に解説していきます。

成熟したワークフロー エンジンの構築は非常に複雑です。この複雑さにどのように対処すればよいでしょうか? 一般的に言えば、次の 3 つの方法があります。

  • 確定的な配信: 要件とは何か、受け入れ基準とは何かを把握し、できればテスト ケースを作成します。このステップは、目標を明確にすることです。
  • 反復開発: 小さな問題セットを解決することから始め、徐々に大きな問題セットを解決するように移行します. ローマは一日にして成らず, 人は一日にして成らず. それにはプロセスが必要です.
  • 分割統治: 大きな問題を小さな問題に分割し、小さな問題の解決が大きな問題の解決を促進します (この考えは多くのシナリオに適用でき、学生はそれらを注意深く経験して理解することができます)。

上記の方法を踏襲し、段階的に詳細に展開すると、本が必要になる場合があります。乾物を失うことなくスペースを削減するために、この記事ではいくつかの重要な繰り返しについて説明し、軽量ワークフロー エンジンの設計と主な実装について詳しく説明します。

では、軽量とは何を意味するのでしょうか。ここでは、主に以下の点を指します

  • 依存関係が少ない: コードの Java 実装では、jdk8 を除き、他のサードパーティの jar パッケージに依存しないため、依存関係によって引き起こされる問題をより軽減できます。
  • カーネル化: 設計では、マイクロカーネル アーキテクチャ モードが採用されており、カーネルは小さく実用的ですが、ある程度のスケーラビリティを提供します。これにより、エンジンをよりよく理解し、適用することができます。
  • 軽量仕様: BPMN 仕様を完全には実装しておらず、BPMN 仕様に従って設計されているわけでもありませんが、仕様を参照するだけで、実装する必要がある要素のごく一部のみを実装しています。その結果、学習コストが削減され、必要に応じて自由にプレイできます。
  • ツール: コードに関しては、単なるツール (UTIL) であり、アプリケーションではありません。そのため、簡単に実行して、独自のデータ レイヤー、ノード レイヤーを拡張し、他のアプリケーションに簡単に統合できます。

OK、ナンセンスです。最初の繰り返しを始めましょう...

 

4.こんにちはProcessEngine

国際的な慣習に従って、最初の反復は hello world を実装するために使用されます。

1.需要

プロセス管理者として、プロセスエンジンで下の画像に示すプロセスを実行して、さまざまな文字列を出力するようにプロセスを構成できるようにしたいと考えています。

 

2.分析

  • 最初のプロセスは Hello ProcessEngine を出力でき、2 番目のプロセスは ProcessEngine Hello を出力できます. 2 つのプロセスの違いは、順序のみが異なることです. 青いノードと赤いノードの機能は変更されていません.
  • 青のノードと赤のノードは両方ともノードであり、それらの機能は異なります。つまり、赤のノードは Hello を出力し、青のノードは ProcessEngine を出力します。
  • 開始ノードと終了ノードは 2 つの特別なノードで、1 つはプロセスを開始し、もう 1 つはプロセスを終了します。
  • ノードは線で結ばれ、ノードが実行された後、次に実行されるノードが矢印で決定されます。
  • 画像ではなく、プロセス、XML、JSON、または何かを表す方法が必要です

3. 設計

(1) プロセスの表現

XML は JSON に比べてセマンティクスが豊富で、より多くの情報を表現できるため、以下に示すように、ここでは XML を使用してプロセスを表現します。

<definitions>
    <process id="process_1" name="hello">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="printHello_1" />
        <printHello id="printHello_1" name="hello">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </printHello>
        <sequenceFlow id="flow_2" sourceRef="printHello_1" targetRef="printProcessEngine_1" />
        <printProcessEngine id="printProcessEngine_1" name="processEngine">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </printProcessEngine>
        <sequenceFlow id="flow_3" sourceRef="printProcessEngine_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_3</incoming>
        </endEvent>
    </process>
</definitions>

 

  • プロセスはプロセスを表す
  • startEvent は開始ノードを表し、endEvent は終了ノードを表します。
  • printHello は、要件の青いノードである hello ノードを出力することを意味します。
  • processEngine は、デマンドの赤いノードである processEngine ノードを出力することを意味します。
  • sequenceFlow は、sourceRef から始まり、targetRef を指す接続を表します。たとえば、flow_3 は、printProcessEngine_1 から endEvent_1 への接続を表します。

(2) ノードの表現

  • 発信は発信エッジを示します。つまり、ノードが実行された後、そのエッジから発信する必要があります。
  • incoming は、着信エッジ、つまり、このノードに入るエッジを表します。
  • ノードには、startEvent のように、発信のみがあり、着信はありません。または、endEvent のように、着信エッジのみがあり、発信エッジがありません。または、printHello、processEngine のように、着信エッジと発信エッジの両方があります。

(3) プロセスエンジンのロジック

上記の XML に基づいて、プロセス エンジンの動作ロジックは次のようになります。

  1. 開始ノード (startEvent) を見つける
  2. startEvent (sequenceFlow) の出力エッジを見つける
  3. エッジ (sequenceFlow) が指すノード (targetRef) を見つける
  4. ノード自体のロジックを実行する
  5. ノードの出力エッジを見つける (sequenceFlow)
  6. 終了ノード (endEvent) に到達するまで 3 ~ 5 を繰り返し、プロセスは終了します。

4.実現する

最初のステップは、データ構造を設計することです。つまり、問題領域の情報をコンピュータ内のデータにマッピングします。

プロセス (PeProcess) は、複数のノード (PeNode) とエッジ (PeEdge) で構成されていることがわかります.ノードには、出力エッジ (out) と入力エッジ (in) があり、エッジには、流入ノード (from) と流出ノードがあります。 (へ) .

具体的な定義は次のとおりです。

public class PeProcess {
    public String id;
    public PeNode start;

    public PeProcess(String id, PeNode start) {
        this.id = id;
        this.start = start;
    }
}

public class PeEdge {
    private String id;
    public PeNode from;
    public PeNode to;

    public PeEdge(String id) {
        this.id = id;
    }
}

public class PeNode {
    private String id;

    public String type;
    public PeEdge in;
    public PeEdge out;

    public PeNode(String id) {
        this.id=id;
    }
}

PS: 主なアイデアを表現するために、コードはより「自由で自由」であり、本番環境で直接コピーして貼り付けることは許可されていません!

次に、次のコードを使用してフローチャートを作成します。

public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml转换为org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子节点中nodeName为process的那个Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node节点上属性为id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();

        Node inPeEdgeNode = XmlUtil.childByName(node, "incoming");
        if (inPeEdgeNode != null)
            //text : 得到inPeEdgeNode的nodeValue
            peNode.in = id2PeEdge.computeIfAbsent(XmlUtil.text(inPeEdgeNode), id -> new PeEdge(id));

        Node outPeEdgeNode = XmlUtil.childByName(node, "outgoing");
        if (outPeEdgeNode != null)
            peNode.out = id2PeEdge.computeIfAbsent(XmlUtil.text(outPeEdgeNode), id -> new PeEdge(id));
    }
}

 

次に、プロセス エンジンのメイン ロジックを実装します。コードは次のようになります。

public class ProcessEngine {
    private String xmlStr;

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public void run() throws Exception {
        PeProcess peProcess = new XmlPeProcessBuilder(xmlStr).build();

        PeNode node = peProcess.start;
        while (!node.type.equals("endEvent")) {
            if ("printHello".equals(node.type))
                System.out.print("Hello ");
            if ("printProcessEngine".equals(node.type))
                System.out.print("ProcessEngine ");

            node = node.out.to;
        }
    }
}

それでおしまい?ワークフロー エンジンは以上ですか。学生の皆さん、簡単に理解してはいけません。結局のところ、これは単なる hello world であり、さまざまなコードの量はすでにかなりの量です。

また、例外制御、一般化、デザインパターンなど、まだまだ改善の余地がたくさんありますが、あくまでもハローワールドであり、学生の理解を促進し、学生が始められるようにすることを目的としています。 .

次に、次のステップは、いくつかの特定の実用的なアプリケーション シナリオに少し近づくことであり、2 回目の反復に進みます。

5.簡易承認

一般的にワークフローエンジンは基盤技術に属し、その上に承認フロー、業務フロー、データフローなどのアプリケーションを構築することができます. それでは、実際の単純な承認シナリオを例として、さらに深化していきましょう.ワークフロー エンジン デザイン、オーケー、さあ、始めましょう。

1.需要

プロセス管理者として、単純な承認フローを実装するようにプロセスを構成できるように、プロセス エンジンで下の画像に示すプロセスを実行したいと考えています。

 

例: Xiao Zhang が申請書を提出し、マネージャーによって承認された後、承認が完了した後、合格したかどうかにかかわらず、結果は Xiao Zhang に第 3 段階を経て送信されます。

2.分析

  • 一般的に言えば、このプロセスはまだ直線的な順序であり、基本的に最後の反復の設計の一部を使用できます。
  • Approval nodes may take a long time, even few days. 次のノードをアクティブに呼び出すワークフロー エンジンのロジックは、このシナリオには適していません。
  • ノード タイプの増加に伴い、ワークフロー エンジンで記述されたノード タイプ フリー ロジックの一部が適切ではなくなりました。
  • 申請書情報、承認者、結果メール通知も承認時に承認結果などの情報が必要であり、これらの情報をどのように伝達するかが課題となる。

3. 設計

  • 登録メカニズムを使用して、ノード タイプとその独自のロジックがワークフロー エンジンに登録されるため、より多くのノードを展開し、ワークフロー エンジンとノードを切り離すことができます。
  • ワークフロー エンジンはパッシブ ドライブ ロジックを追加します。これにより、ワークフロー エンジンは外部ノードを介して次のノードを実行できます。
  • コンテキストセマンティクスを追加し、それをグローバル変数として使用して、データが各ノードを通過できるようにします

4.実現する

新しい XML 定義は次のとおりです。

<definitions>
    <process id="process_2" name="简单审批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1" />
        <approvalApply id="approvalApply_1" name="提交申请单">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1" />
        <approval id="approval_1" name="审批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="notify_1"/>
        <notify id="notify_1" name="结果邮件通知">
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
        </notify>
        <sequenceFlow id="flow_4" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_4</incoming>
        </endEvent>
    </process>
</definitions>

まず、変数を渡すためのコンテキスト オブジェクト クラスが必要です。これは次のように定義されます。

public class PeContext {
    private Map<String, Object> info = new ConcurrentHashMap<>();

    public Object getValue(String key) {
        return info.get(key);
    }

    public void putValue(String key, Object value) {
        info.put(key, value);
    }
}

各ノードの処理ロジックは異なり、ここで特定の抽象化を実行する必要があります. プロセスにおけるノードの役割が論理処理であることを強調するために、次のように定義される新しい型演算子が導入されています:

public interface IOperator {
    //引擎可以据此来找到本算子
    String getType();

    //引擎调度本算子
    void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext);
}

エンジンの場合、ノードに遭遇したときにスケジュールする必要がありますが、どのようにスケジュールするのでしょうか? 最初に、各ノード オペレーターを登録する必要があります (registerNodeProcessor())。これにより、スケジュールするオペレーターを見つけることができます。

次に、エンジンはノード オペレータ自身のロジックが処理されたことをどのように認識しますか? 一般的に言えば、エンジンは認識せず、オペレーターによってのみエンジンに伝えることができるため、エンジンはオペレーターによって呼び出される関数 (nodeFinished()) を提供する必要があります。

最後に、オペレータ タスクのスケジューリングをエンジンのドライバから分離し、それらを別のスレッドに配置します。

変更された ProcessEngine コードは次のとおりです。

public class ProcessEngine {
    private String xmlStr;

    //存储算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任务数据暂存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任务调度线程
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注册到引擎中,便于引擎调用之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.out.to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(String peNodeID) {
        PeNode node = peProcess.peNodeWithID(peNodeID);
        executeNode(node.out.to);
    }
}

次に、この例に必要な 3 つの演算子の単純な (初歩的な) 実装です。コードは次のようになります。

/**
 * 提交申请单
 */
public class OperatorOfApprovalApply implements IOperator {
    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("form", "formInfo");
        peContext.putValue("applicant", "小张");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 审批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "经理");
        peContext.putValue("message", "审批通过");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 结果邮件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申请单 %s 被 %s 审批,结果为 %s",
                peContext.getValue("applicant"),
                peContext.getValue("form"),
                peContext.getValue("approver"),
                peContext.getValue("message")));

        processEngine.nodeFinished(node.id);
    }
}

実行して結果を確認します。コードは次のとおりです。

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //读取文件内容到字符串
        String modelStr = Tools.readResoucesFile("model/two/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());

        processEngine.start();

        Thread.sleep(1000 * 1);

    }

}

 

小张 提交的申请单 formInfo 被 经理 审批,结果为 审批通过
process finished!

 

この時点で、軽量ワークフロー エンジンのコア ロジックはほぼ導入されています. しかし、シーケンス構造のみをサポートするには薄すぎます. プログラム フローの 3 つの基本構造は、シーケンス、分岐、およびループであることがわかっています. これらを使用して、 3 構造体は、基本的にほとんどのプロセス ロジックを表すことができます。ループは結合された構造と見なすことができます, つまり: ループはシーケンスとブランチから派生することができます. シーケンスを実装したら, ブランチを実装するだけでよく, などの多くのタイプのブランチがあります. : 2 つから 1 つを選択、N から 1 つを選択、N は M を選択 (1<=M<=N)、ここで、N は 1 つを選択し、2 つの選択肢の組み合わせから派生できるものを選択し、N は M を選択して、2 つの選択肢の組み合わせから派生することもできます2 つの選択肢がありますが、より冗長で直感的ではありません. したがって、2 つの選択肢の分岐を実装する限り、ほとんどのプロセス ロジック シナリオを満たすことができます. さて、3 回目の繰り返しが始まります.

 

6. 一般承認

プロセス管理者として、一般的な承認フローを実装するようにプロセスを構成できるように、プロセス エンジンで下の画像に示すプロセスを実行したいと考えています。

 

例: Xiao Zhang が申請書を提出し、マネージャーによって承認されます.承認が完了した後、申請が承認された場合、電子メール通知が送信されます.申請が承認されなかった場合、彼は電話をかけ直します.承認されるまで申請書を提出します。

1.分析

  • シンプルな双方向転送を実行できるブランチ ノードを導入する必要があります。
  • ノードには複数の着信エッジと発信エッジがあります
  • 分岐ノードを構成できる論理式セマンティクスが必要です

2. 設計

  • ノードは複数入力エッジと複数出力エッジをサポートする必要があります
  • どの発信エッジから発信するかを決定するノード オペレーター
  • 単純な論理式の解析をサポートする単純なルール エンジンを使用する
  • 単純分岐ノードの XML 定義

3.実現する

新しい XML 定義は次のとおりです。

<definitions>
    <process id="process_2" name="简单审批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1"/>
        <approvalApply id="approvalApply_1" name="提交申请单">
            <incoming>flow_1</incoming>
            <incoming>flow_5</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1"/>
        <approval id="approval_1" name="审批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="simpleGateway_1"/>
        <simpleGateway id="simpleGateway_1" name="简单是非判断">
            <trueOutGoing>flow_4</trueOutGoing>
            <expr>approvalResult</expr>
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
            <outgoing>flow_5</outgoing>
        </simpleGateway>
        <sequenceFlow id="flow_5" sourceRef="simpleGateway_1" targetRef="approvalApply_1"/>
        <sequenceFlow id="flow_4" sourceRef="simpleGateway_1" targetRef="notify_1"/>
        <notify id="notify_1" name="结果邮件通知">
            <incoming>flow_4</incoming>
            <outgoing>flow_6</outgoing>
        </notify>
        <sequenceFlow id="flow_6" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_6</incoming>
        </endEvent>
    </process>
</definitions>

 

その中で、単純な分岐ノード simpleGateway を追加して、単純な代替分岐を表現します. expr の式が true の場合は trueOutGoing の出力エッジに移動し、そうでない場合は別の出力エッジに移動します.

このノードは、複数の入力エッジと複数の出力エッジをサポートします。変更された PeNode は次のとおりです。

public class PeNode {
    public String id;

    public String type;
    public List<PeEdge> in = new ArrayList<>();
    public List<PeEdge> out = new ArrayList<>();
    public Node xmlNode;

    public PeNode(String id) {
        this.id = id;
    }

    public PeEdge onlyOneOut() {
        return out.get(0);
    }

    public PeEdge outWithID(String nextPeEdgeID) {
        return out.stream().filter(e -> e.id.equals(nextPeEdgeID)).findFirst().get();
    }

    public PeEdge outWithOutID(String nextPeEdgeID) {
        return out.stream().filter(e -> !e.id.equals(nextPeEdgeID)).findFirst().get();
    }

}

以前は、出力エッジが 1 つしかない場合、現在のノードが次のノードを決定していました。現在、出力エッジがさらに多くなり、次のノードが何であるかを決定するのはエッジ次第です。変更されたプロセス エンジン コードは次のようになります。 :

public class ProcessEngine {
    private String xmlStr;

    //存储算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任务数据暂存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任务调度线程
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注册到引擎中,便于引擎调用之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.onlyOneOut().to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(PeEdge nextPeEdgeID) {
        executeNode(nextPeEdgeID.to);
    }
}

新しく追加された simpleGateway ノード オペレーターは次のとおりです。

/**
 * 简单是非判断
 */
public class OperatorOfSimpleGateway implements IOperator {
    @Override
    public String getType() {
        return "simpleGateway";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        engine.put("approvalResult", peContext.getValue("approvalResult"));

        String expression = XmlUtil.childTextByName(node.xmlNode, "expr");
        String trueOutGoingEdgeID = XmlUtil.childTextByName(node.xmlNode, "trueOutGoing");

        PeEdge outPeEdge = null;
        try {
            outPeEdge = (Boolean) engine.eval(expression) ?
                    node.outWithID(trueOutGoingEdgeID) : node.outWithOutID(trueOutGoingEdgeID);
        } catch (ScriptException e) {
            e.printStackTrace();
        }

        processEngine.nodeFinished(outPeEdge);
    }
}

その中で、jsスクリプトは単に式として使用されているだけで、もちろんデメリットはここでは展開されていません。

学生の CC+CV を容易にするために、それに応じて変更された他のコードは次のとおりです。

/**
 * 审批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "经理");

        Integer price = (Integer) peContext.getValue("price");
        //价格<=200审批才通过,即:approvalResult=true
        boolean approvalResult = price <= 200;
        peContext.putValue("approvalResult", approvalResult);

        System.out.println("approvalResult :" + approvalResult + ",price : " + price);

        processEngine.nodeFinished(node.onlyOneOut());
    }
}

/**
 * 提交申请单
 */
public class OperatorOfApprovalApply implements IOperator {

    public static int price = 500;

    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        //price每次减100
        peContext.putValue("price", price -= 100);
        peContext.putValue("applicant", "小张");

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


/**
 * 结果邮件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申请单 %s 被 %s 审批,结果为 %s",
                peContext.getValue("applicant"),
                peContext.getValue("price"),
                peContext.getValue("approver"),
                peContext.getValue("approvalResult")));

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml转换为org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子节点中nodeName为process的那个Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node节点上属性为id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();
        peNode.xmlNode = node;

        List<Node> inPeEdgeNodes = XmlUtil.childsByName(node, "incoming");
        inPeEdgeNodes.stream().forEach(n -> peNode.in.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));

        List<Node> outPeEdgeNodes = XmlUtil.childsByName(node, "outgoing");
        outPeEdgeNodes.stream().forEach(n -> peNode.out.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));
    }
}

実行して結果を確認します。コードは次のとおりです。

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //读取文件内容到字符串
        String modelStr = Tools.readResoucesFile("model/third/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());
        processEngine.registNodeProcessor(new OperatorOfSimpleGateway());

        processEngine.start();

        Thread.sleep(1000 * 1);
    }

}

 

approvalResult :false,price : 400
approvalResult :false,price : 300
approvalResult :true,price : 200
小张 提交的申请单 200  经理 审批,结果为 true
process finished!

 

これまでのところ、この要件の実装は完了しており、分岐セマンティクスを直接実装するだけでなく、ループ セマンティクスも間接的に実装されていることがわかります。

軽量ワークフローエンジンとしては、基本的にはこれで終わりなので、次はまとめと展望を作っていきましょう。

 

7. まとめと展望

上記の 3 回の反復の後、次の図に示すように、比較的安定したワークフロー エンジン構造を得ることができます。

 

この図から、比較的安定したエンジン レイヤーがあり、スケーラビリティを提供するためにノード オペレーター レイヤーが提供され、ノード オペレーターのすべての追加がここにあることがわかります。

また、ある程度の制御の逆転があります。つまり、エンジンではなく、オペレータが次にどこに行くかを決定します。このようにして、エンジンの柔軟性が大幅に向上し、カプセル化が向上します。

最後に、ノード間のデータの流れを容易にするグローバル変数のメカニズムを提供するコンテキストが使用されます。

もちろん、上記の 3 つの反復は実際のオンライン アプリケーション シナリオとはかけ離れており、次の点を実現し、期待する必要があります。

  • いくつかの異常事態の考慮と設計
  • ノードは、入力パラメーター、出力パラメーター、データ型などを使用して関数に抽象化する必要があります。
  • エンジンまたはスピットイベントを制御するために、重要な場所に埋め込みポイントを追加します
  • グラフのセマンティック正当性チェック、xsd、カスタム チェック手法など。
  • グラフ ダグ アルゴリズムの検出
  • プロセスのプロセス履歴、および任意のノードへのロールバック
  • フローチャートの動的変更。つまり、プロセスの開始後にフローチャートを変更できます。
  • 同時変更の場合の考慮事項
  • 効率に関する考慮事項
  • 再起動後に循環情報が失われないようにするには、永続化メカニズムの追加が必要です
  • 処理のキャンセル、リセット、変数入力など
  • より適切なルール エンジンと複数のルール エンジンの実装と構成
  • フロントエンド キャンバス、フロントエンドおよびバックエンド プロセスのデータ構造の定義と変換

 

作者: リウ・ヤン

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/5580501