Diseño e implementación del motor de flujo de trabajo ligero

1. ¿Qué es un motor de flujo de trabajo?

Un motor de flujo de trabajo es un conjunto de código que impulsa la ejecución del flujo de trabajo.

En cuanto a qué es el flujo de trabajo, por qué hay un flujo de trabajo y los escenarios de aplicación del flujo de trabajo, los estudiantes pueden consultar la información en Internet, pero no la expandiré aquí.

 

2. ¿Por qué repetir la rueda?

Hay muchos motores de flujo de trabajo de código abierto, como activiti, flowable, Camunda, etc., entonces, ¿por qué no elegirlos? Con base en las siguientes consideraciones:

  • Lo que es más importante, no puede satisfacer las necesidades comerciales y no se pueden realizar algunos escenarios especiales.
  • Algunos requisitos son más complicados de implementar. Además, es necesario modificar directamente la base de datos del motor, lo que trae consigo enormes peligros ocultos para el funcionamiento estable del motor, y también crea algunas dificultades para la futura actualización de la versión del motor.
  • Hay muchos materiales, volumen de código y API, el costo de aprendizaje es alto y la capacidad de mantenimiento es deficiente.
  • Después del análisis y evaluación, nuestro escenario de negocio requiere menos elementos BPMN, y el costo de desarrollo e implementación no es alto.

Por lo tanto, al reinventar la rueda, de hecho, hay una consideración estratégica más profunda, es decir: como empresa de tecnología, ¡debemos tener nuestra propia tecnología subyacente central! De esta manera, no puede ser controlado por personas (consulte el problema reciente del chip).

 

3. Cómo hacer la rueda

Para un intercambio de aprendizaje es más importante el proceso que el resultado, aquellos intercambios que solo hablan del resultado, no del proceso o incluso lo hablan en detalle, creo que es una muestra de músculos, no un verdadero intercambio. Por lo tanto, a continuación, este artículo se centrará en describir el proceso principal de fabricación de una rueda.

La construcción de un motor de flujo de trabajo maduro es muy compleja, ¿cómo lidiar con esta complejidad? En términos generales, hay tres métodos:

  • Entrega determinista: averigüe cuáles son los requisitos, cuáles son los criterios de aceptación y, preferiblemente, escriba casos de prueba.Este paso es para aclarar los objetivos.
  • Desarrollo iterativo: comience con la resolución de conjuntos de problemas pequeños y haga una transición gradual a la resolución de conjuntos de problemas grandes. Roma no se construyó en un día, y las personas no maduran en un día. Se necesita un proceso.
  • Divide y vencerás: divide los problemas grandes en problemas pequeños, y la solución de los problemas pequeños promoverá la solución de los problemas grandes (esta idea es aplicable a muchos escenarios y los estudiantes pueden experimentarlos y comprenderlos cuidadosamente).

Si sigue el método anterior y lo amplía paso a paso en detalle, es posible que necesite un libro. Para reducir el espacio sin perder los productos secos, este artículo describirá varias iteraciones clave y luego elaborará el diseño y la implementación principal del motor de flujo de trabajo liviano.

Entonces, ¿qué significa ligero? Aquí, se refiere principalmente a los siguientes puntos

  • Menos dependencias: en la implementación java del código, excepto jdk8, no depende de otros paquetes jar de terceros, lo que puede reducir mejor los problemas causados ​​por las dependencias.
  • Kernelización: en el diseño, se adopta el modo de arquitectura de micro-kernel, y el kernel es pequeño y práctico, al tiempo que proporciona un cierto grado de escalabilidad. De este modo, el motor se puede comprender y aplicar mejor.
  • Especificación ligera: No implementa completamente la especificación BPMN, ni está diseñado de acuerdo con la especificación BPMN, sino que solo hace referencia a la especificación, y solo implementa una pequeña parte de los elementos que deben implementarse. Como resultado, el costo de aprendizaje se reduce y puedes jugar libremente según tus necesidades.
  • Herramientas: en términos de código, es solo una herramienta (UTIL), no una aplicación. Así que simplemente puede ejecutarlo, expandir su propia capa de datos, capa de nodo e integrarse más fácilmente en otras aplicaciones.

OK, basta de tonterías, comencemos la primera iteración...

 

4. Hola ProcessEngine

De acuerdo con la convención internacional, la primera iteración se usa para implementar hello world.

1. Demanda

Como administrador de procesos, me gustaría que el motor de procesos ejecutara el proceso que se muestra en la imagen a continuación para poder configurar el proceso para imprimir diferentes cadenas.

 

2. Análisis

  • El primer proceso puede imprimir Hello ProcessEngine, y el segundo proceso puede imprimir ProcessEngine Hello. La diferencia entre los dos procesos es que solo el orden es diferente. Las funciones de los nodos azules y los nodos rojos no han cambiado.
  • El nodo azul y el nodo rojo son ambos nodos y sus funciones son diferentes, es decir: el nodo rojo imprime Hello y el nodo azul imprime ProcessEngine
  • Los nodos de inicio y finalización son dos nodos especiales, uno para iniciar el proceso y otro para finalizar el proceso.
  • Los nodos están conectados por líneas.Después de que se ejecuta un nodo, el próximo nodo que se ejecutará está determinado por la flecha.
  • Necesita una forma de representar el proceso, o XML, o JSON, o algo así, no imágenes

3. Diseño

(1) Representación del proceso

En comparación con JSON, XML tiene una semántica más rica y puede expresar más información, por lo que XML se usa aquí para representar el proceso, como se muestra a continuación.

<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>

 

  • proceso representa un proceso
  • startEvent representa el nodo de inicio y endEvent representa el nodo final
  • printHello significa imprimir el nodo hola, que es el nodo azul en el requisito
  • processEngine significa imprimir el nodo processEngine, que es el nodo rojo en la demanda
  • secuenciaFlow representa una conexión, comenzando desde sourceRef y apuntando a targetRef, por ejemplo: flow_3, que representa una conexión desde printProcessEngine_1 hasta endEvent_1.

(2) Representación de nodos

  • Saliente indica el borde saliente, es decir, después de que se ejecuta el nodo, debe salir de ese borde.
  • incoming representa el borde entrante, es decir, desde qué borde entrar en este nodo.
  • Un nodo solo tiene salientes pero no entrantes, como: startEvent, o puede tener solo bordes entrantes pero no salientes, como: endEvent, o puede tener bordes entrantes y salientes, como: printHello, processEngine.

(3) La lógica del motor de proceso

Según el XML anterior, la lógica de funcionamiento del motor de proceso es la siguiente

  1. Encuentre el nodo de inicio (startEvent)
  2. Encuentre el borde saliente de startEvent (sequenceFlow)
  3. Encuentre el nodo (targetRef) al que apunta el borde (sequenceFlow)
  4. Ejecutar la lógica del propio nodo.
  5. Encuentre el borde saliente del nodo (sequenceFlow)
  6. Repita 3-5 hasta que se encuentre el nodo final (endEvent) y el proceso termine

4. Date cuenta

El primer paso es diseñar la estructura de datos, es decir, mapear la información en el dominio del problema a los datos en la computadora.

Se puede ver que un proceso (PeProcess) consta de múltiples nodos (PeNode) y aristas (PeEdge). Los nodos tienen aristas de salida (out) y aristas de entrada (in), y las aristas tienen nodos de entrada (from) y nodos de salida (a). .

Las definiciones específicas son las siguientes:

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;
    }
}

PD: Para expresar la idea principal, el código es más "sin restricciones y libre", ¡y no está permitido copiar y pegar directamente en producción!

A continuación, cree un diagrama de flujo con el siguiente código:

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));
    }
}

 

A continuación, implemente la lógica principal del motor de procesos, el código es el siguiente:

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;
        }
    }
}

¿Eso es todo? ¿Eso es todo para el motor de flujo de trabajo? Estudiantes, no lo entiendan tan simplemente, después de todo, esto es solo hola mundo, y la cantidad de varios códigos ya es bastante.

Además, todavía hay mucho margen de mejora, como el control de excepciones, la generalización, los patrones de diseño, etc., pero después de todo, es solo un hola mundo, el propósito es facilitar la comprensión de los estudiantes y permitirles comenzar. .

Luego, el siguiente paso es acercarse un poco más a algunos escenarios de aplicación práctica específicos, y continuamos con la segunda iteración.

5. Aprobación sencilla

En términos generales, el motor de flujo de trabajo pertenece a la tecnología subyacente y se pueden construir aplicaciones de flujo de aprobación, flujo comercial, flujo de datos, etc.. Luego, tomemos un escenario de aprobación simple en la práctica como ejemplo y sigamos profundizando. el motor de flujo de trabajo Diseño, ok, aquí vamos.

1. Demanda

Como administrador de procesos, me gustaría que el motor de procesos ejecutara el proceso que se muestra en la imagen a continuación para poder configurar el proceso para implementar un flujo de aprobación simple.

 

Por ejemplo: Xiao Zhang envía un formulario de solicitud y luego el gerente lo aprueba. Una vez que se completa la aprobación, ya sea que se apruebe o no, el resultado se enviará a Xiao Zhang a través del tercer paso.

2. Análisis

  • En términos generales, este proceso todavía está en un orden lineal, y básicamente se puede usar parte del diseño de la última iteración.
  • Los nodos de aprobación pueden tardar mucho tiempo, incluso varios días. La lógica del motor de flujo de trabajo que llama activamente al siguiente nodo no es adecuada para este escenario.
  • Con el aumento de los tipos de nodos, la parte de la lógica libre de tipos de nodos escrita en el motor de flujo de trabajo no es adecuada.
  • La información del formulario de solicitud, el aprobador y la notificación por correo electrónico del resultado también necesitan el resultado de la aprobación y otra información durante la aprobación. La forma de transmitir esta información también es un problema a considerar.

3. Diseño

  • Con el mecanismo de registro, el tipo de nodo y su propia lógica se registran en el motor de flujo de trabajo, de modo que se pueden expandir más nodos y el motor de flujo de trabajo y los nodos se pueden desacoplar.
  • El motor de flujo de trabajo agrega lógica de control pasivo, lo que permite que el motor de flujo de trabajo ejecute el siguiente nodo a través de un dispositivo externo.
  • Agregue semántica de contexto y utilícela como una variable global para permitir que los datos fluyan a través de cada nodo

4. Date cuenta

La nueva definición XML es la siguiente:

<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>

En primer lugar, debe haber una clase de objeto de contexto para pasar variables, que se define de la siguiente manera:

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);
    }
}

La lógica de procesamiento de cada nodo es diferente, y aquí se debe llevar a cabo cierta abstracción.Para enfatizar que el papel de los nodos en el proceso es el procesamiento lógico, se introduce un nuevo tipo-operador, que se define de la siguiente manera:

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

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

Para el motor, cuando encuentra un nodo, debe programarse, pero ¿cómo programarlo? En primer lugar, cada operador de nodo debe registrarse (registerNodeProcessor()), de modo que se pueda encontrar el operador que se programará.

En segundo lugar, ¿cómo sabe el motor que se ha procesado la propia lógica del operador del nodo? En términos generales, el motor no sabe, y solo el operador puede informarle al motor, por lo que el motor debe proporcionar una función (nodeFinished()), a la que llama el operador.

Finalmente, desacople la programación de las tareas del operador del controlador del motor y colóquelas en diferentes subprocesos.

El código de ProcessEngine modificado es el siguiente:

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);
    }
}

A continuación, la implementación simple (rudimentaria) de los tres operadores requeridos para este ejemplo, el código es el siguiente:

/**
 * 提交申请单
 */
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);
    }
}

Ejecútelo y vea cuál es el resultado, el código es el siguiente:

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!

 

En este punto, la lógica central del motor de flujo de trabajo ligero casi se introduce. Sin embargo, es demasiado delgado para admitir solo la estructura de secuencia. Sabemos que las tres estructuras básicas del flujo del programa son secuencia, rama y bucle. Con estos tres La estructura básicamente puede representar la mayor parte de la lógica del proceso. El ciclo se puede considerar como una estructura combinada, es decir: el ciclo se puede derivar de la secuencia y la rama.Hemos implementado la secuencia, luego solo necesitamos implementar la rama, y ​​hay muchos tipos de ramas, como : elige uno de dos, elige uno de N , N elige M (1<=M<=N), donde N elige uno puede derivarse de la combinación de dos alternativas, y N elige M también puede derivarse de la combinación de dos alternativas, pero es más detallado y no tan intuitivo. Por lo tanto, podemos satisfacer la mayoría de los escenarios lógicos del proceso siempre que implementemos la rama de dos opciones. Bueno, comienza la tercera iteración.

 

6. Aprobación general

Como administrador de procesos, me gustaría que el motor de procesos ejecutara el proceso que se muestra en la imagen a continuación para poder configurar el proceso para implementar un flujo de aprobación general.

 

Por ejemplo: Xiao Zhang envía un formulario de solicitud, que luego es aprobado por el gerente. Una vez que se completa la aprobación, si se aprueba la solicitud, se enviará una notificación por correo electrónico. Si la solicitud no se aprueba, volverá a llamar y volverá a escribir el formulario de solicitud hasta que sea aprobado.

1. Análisis

  • Es necesario introducir un nodo de rama, que puede realizar una transferencia bidireccional simple
  • Los nodos tienen más de un borde entrante y saliente
  • Requiere una semántica de expresión lógica que pueda configurar nodos de rama

2. Diseño

  • Los nodos deben admitir múltiples bordes de entrada y salida
  • Operador de nodo para decidir de qué borde saliente salir
  • Use un motor de reglas simple que admita el análisis de expresiones lógicas simples
  • Definición XML de nodo de rama simple

3. Date cuenta

La nueva definición XML es la siguiente:

<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>

 

Entre ellos, se agrega el nodo de rama simple simpleGateway para representar una rama alternativa simple.Cuando la expresión en expr es verdadera, vaya al borde de salida en trueOutGoing, de lo contrario, vaya al otro borde de salida.

El nodo admite varios bordes de entrada y varios bordes de salida. El PeNode modificado es el siguiente:

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();
    }

}

En el pasado, cuando solo había un borde saliente, el nodo actual determinaba el siguiente nodo. Ahora que hay más bordes salientes, depende del borde decidir cuál es el siguiente nodo. El código del motor de proceso modificado es el siguiente :

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);
    }
}

El operador de nodo simpleGateway recién agregado es el siguiente:

/**
 * 简单是非判断
 */
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);
    }
}

Entre ellos, el script js se usa simplemente como una expresión, por supuesto, las desventajas no se amplían aquí.

Para facilitar a los estudiantes CC+CV, otros códigos que han cambiado en consecuencia son los siguientes:

/**
 * 审批
 */
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))));
    }
}

Ejecútelo y vea cuál es el resultado, el código es el siguiente:

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!

 

Hasta ahora, la implementación de este requisito se ha completado Además de implementar directamente la semántica de rama, podemos ver que la semántica de bucle también se implementa indirectamente aquí.

Como un motor de flujo de trabajo liviano, este es básicamente el final de la historia A continuación, hagamos un resumen y una perspectiva.

 

7. Resumen y perspectiva

Después de las tres iteraciones anteriores, podemos obtener una estructura de motor de flujo de trabajo relativamente estable, como se muestra en la siguiente figura:

 

A partir de esta figura, podemos ver que hay una capa de motor relativamente estable y, para proporcionar escalabilidad, se proporciona una capa de operador de nodo y todas las adiciones de operadores de nodo están aquí.

Además, existe cierto grado de inversión de control, es decir, el operador decide a dónde ir a continuación, no el motor. De esta forma, la flexibilidad del motor mejora considerablemente y la encapsulación es mejor.

Finalmente, se utilizan contextos, proporcionando un mecanismo para variables globales para facilitar el flujo de datos entre nodos.

Por supuesto, las tres iteraciones anteriores están lejos de los escenarios de aplicación en línea reales, y los siguientes puntos deben darse cuenta y esperarse, de la siguiente manera:

  • Consideración y diseño de algunas situaciones anormales.
  • El nodo debe abstraerse en una función, con parámetros de entrada, parámetros de salida, tipos de datos, etc.
  • Agregue puntos enterrados en lugares clave para controlar el motor o escupir eventos
  • Comprobación de legitimidad semántica de grafos, xsd, técnicas de comprobación personalizadas, etc.
  • Detección del algoritmo Graph dag
  • Procesar el historial del proceso y retroceder a cualquier nodo
  • Modificación dinámica del diagrama de flujo, es decir, el diagrama de flujo se puede modificar después de que se inicia el proceso
  • Consideraciones en el caso de modificaciones concurrentes
  • Consideraciones de eficiencia
  • Para evitar la pérdida de información de circulación después del reinicio, se requiere la adición de un mecanismo de persistencia
  • Cancelación de procesos, reset, entrada de variables, etc.
  • Motor de reglas más adecuado e implementación y configuración de múltiples motores de reglas
  • Definición y conversión de la estructura de datos del proceso front-end, front-end y back-end

 

Autor: Liu Yang

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/5580501
Recomendado
Clasificación