一失尖、前言:
發(fā)現(xiàn)網(wǎng)上關(guān)于Flowable的資料基本都是淺嘗輒止,對(duì)如何構(gòu)建一個(gè)企業(yè)級(jí)的流程應(yīng)用說明很少渐苏,所以寫個(gè)實(shí)戰(zhàn)系列掀潮,希望對(duì)大家和自己,都有所幫助整以。
二胧辽、認(rèn)識(shí)Flowable
Flowable是一個(gè)使用Java編寫的輕量級(jí)業(yè)務(wù)流程引擎。
Flowable流程引擎可用于部署B(yǎng)PMN 2.0流程定義公黑,創(chuàng)建這些流程定義的流程實(shí)例,進(jìn)行查詢人断,訪問運(yùn)行中或歷史的流程實(shí)例與相關(guān)數(shù)據(jù)恶迈。
Flowable是Activiti的fork步做,即Flowable源自Activiti全度。所以可以看到将鸵,F(xiàn)lowable很多設(shè)計(jì)與實(shí)現(xiàn)顶掉,與Activiti是相同的痒筒。
開源Flowable官網(wǎng):https://www.flowable.com/open-source/
三凸克、完整實(shí)例
Flowable官網(wǎng)教程已經(jīng)提供了一個(gè)很簡(jiǎn)單的流程運(yùn)行例子萎战,是英文版蚂维。
下面的例子內(nèi)容來源于官網(wǎng)教程虫啥,已經(jīng)看過的同學(xué)可以直接跳過到下一節(jié)內(nèi)容
。
我們將構(gòu)建的例子是一個(gè)簡(jiǎn)單的請(qǐng)假(holiday request)流程:
雇員(employee)申請(qǐng)幾天的假期
經(jīng)理(manager)批準(zhǔn)或駁回申請(qǐng)
我們會(huì)模擬將申請(qǐng)注冊(cè)到某個(gè)外部系統(tǒng)砸抛,并給雇員發(fā)送結(jié)果郵件
1.創(chuàng)建項(xiàng)目
打開IDEA景东,通過File -> New -> Project... -> Maven 創(chuàng)建一個(gè)新的Maven項(xiàng)目
在下一個(gè)界面斤吐,填入項(xiàng)目名Name庄呈,GroupId 和 ArtifactId 默認(rèn)即可
這樣就建立了空的Maven項(xiàng)目抒痒。
2.創(chuàng)建流程引擎
為項(xiàng)目添加3個(gè)依賴:
Flowable流程引擎。使我們可以創(chuàng)建一個(gè)ProcessEngine流程引擎對(duì)象傀广,并訪問Flowable API誓酒。
一個(gè)內(nèi)存數(shù)據(jù)庫靠柑。本例中為H2歼冰,因?yàn)镕lowable引擎在運(yùn)行流程實(shí)例時(shí)隔嫡,需要使用數(shù)據(jù)庫來存儲(chǔ)執(zhí)行與歷史數(shù)據(jù)腮恩。
日志框架秸滴。Flowable使用SLF4J作為內(nèi)部日志框架荡含。本例中内颗,我們使用log4j作為SLF4J的實(shí)現(xiàn)均澳。
在pom.xml文件中添加下列行:
<dependencies>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>6.7.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.176</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
Log4j需要一個(gè)配置文件糟袁。在src/main/resources文件夾下添加log4j.properties文件项戴,并寫入下列內(nèi)容:
log4j.rootLogger=INFO, CA
log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n
注意: 關(guān)于Log4j 的遠(yuǎn)程代碼執(zhí)行漏洞問題界斜,影響范圍是Apache Log4j 2.x <= 2.14.1 版本项贺,所以上面的日志版本是安全的开缎。
有時(shí)候奕删,依賴JAR無法自動(dòng)獲取急侥,可以右鍵點(diǎn)擊項(xiàng)目坏怪,并選擇 Maven ->Reload Project以強(qiáng)制手動(dòng)刷新铝宵。
創(chuàng)建一個(gè)新的Java類鹏秋,并添加標(biāo)準(zhǔn)的Java main方法:
public class HolidayRequest {
public static void main(String[] args) {
}
}
首先要做的是初始化ProcessEngine流程引擎實(shí)例侣夷。創(chuàng)建一個(gè)ProcessEngineConfiguration實(shí)例 琴锭,并配置數(shù)據(jù)庫JDBC連接决帖。然后由ProcessEngineConfiguration創(chuàng)建ProcessEngine實(shí)例地回。
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
public class HolidayRequest {
public static void main(String[] args) {
ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1")
.setJdbcUsername("sa")
.setJdbcPassword("")
.setJdbcDriver("org.h2.Driver")
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
ProcessEngine processEngine = cfg.buildProcessEngine();
}
}
注意:內(nèi)存H2數(shù)據(jù)庫在JVM重啟后會(huì)消失刻像。如果需要永久保存數(shù)據(jù)绎速,需要切換為持久化數(shù)據(jù)庫洒宝,并相應(yīng)切換連接參數(shù)雁歌。
這樣就可以運(yùn)行了比庄。在IDEA中最簡(jiǎn)單的方法是右鍵點(diǎn)擊類文件佳窑,選擇Run 'HolidayRequest.main()' :
成功運(yùn)行,應(yīng)該可以看到關(guān)于引擎啟動(dòng)與創(chuàng)建數(shù)據(jù)庫表結(jié)構(gòu)的提示日志:
這樣就得到了一個(gè)啟動(dòng)可用的流程引擎溉委。接下來為它提供一個(gè)流程瓣喊!
3.創(chuàng)建流程定義文件
我們要構(gòu)建的流程是一個(gè)非常簡(jiǎn)單的請(qǐng)假流程八匠。 在Flowable術(shù)語中梨树,我們將其稱為一個(gè)流程定義(process definition)。一個(gè)流程定義可以啟動(dòng)多個(gè)流程實(shí)例(process instance)仗谆。流程定義可以看做是重復(fù)執(zhí)行流程的藍(lán)圖藻雪。 在這個(gè)例子中勉耀,流程定義定義了請(qǐng)假的各個(gè)步驟便斥,而一個(gè)流程實(shí)例對(duì)應(yīng)某個(gè)雇員提出的一個(gè)請(qǐng)假申請(qǐng)。
我們要使用的流程定義為:
為了明確起見晋渺,說明一下幾個(gè)要點(diǎn):
啟動(dòng)流程需要提供一些信息,例如雇員名字俭厚、請(qǐng)假時(shí)長(zhǎng)以及說明叼丑。鸠信。
左側(cè)的圓圈叫做啟動(dòng)事件(start event)星立。這是一個(gè)流程實(shí)例的起點(diǎn)室奏。
第一個(gè)矩形是一個(gè)用戶任務(wù)(user task)胧沫。這是流程中人類用戶操作的步驟绒怨。在這個(gè)例子中,經(jīng)理需要批準(zhǔn)或駁回申請(qǐng)六剥。
取決于經(jīng)理的決定仗考,排他網(wǎng)關(guān)(exclusive gateway) (帶叉的菱形)會(huì)將流程實(shí)例路由至批準(zhǔn)或駁回路徑。
如果批準(zhǔn)顿膨,則需要將申請(qǐng)注冊(cè)至某個(gè)外部系統(tǒng),并跟著另一個(gè)用戶任務(wù)囊咏,將經(jīng)理的決定通知給申請(qǐng)人梅割。
如果駁回户辞,則為雇員發(fā)送一封郵件通知他刃榨。
注意:一般來說枢希,這樣的流程定義使用可視化建模工具建立苞轿,如Flowable Designer(Eclipse)或Flowable Web Modeler(Web應(yīng)用)。IDEA沒有官方提供的可視化插件秀睛,可選插件為Flowable BPMN visualizer蹂安。
將下面的XML保存在src/main/resources文件夾下名為holiday-request.bpmn20.xml的文件中田盈。
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
xmlns:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="holidayRequest" name="Holiday Request" isExecutable="true">
<startEvent id="startEvent"/>
<sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
<userTask id="approveTask" name="Approve or reject request"/>
<sequenceFlow sourceRef="approveTask" targetRef="decision"/>
<exclusiveGateway id="decision"/>
<sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${approved}
]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="decision" targetRef="sendRejectionMail">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${!approved}
]]>
</conditionExpression>
</sequenceFlow>
<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="CallExternalSystemDelegate"/>
<sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
<userTask id="holidayApprovedTask" name="Holiday approved"/>
<sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
<serviceTask id="sendRejectionMail" name="Send out rejection email"
flowable:class="SendRejectionMail"/>
<sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>
<endEvent id="approveEnd"/>
<endEvent id="rejectEnd"/>
</process>
</definitions>
每一個(gè)步驟(在BPMN 2.0術(shù)語中稱作活動(dòng)(activity))都有一個(gè)id屬性述暂,為其提供一個(gè)在XML文件中唯一的標(biāo)識(shí)符畦韭。所有的活動(dòng)都可以設(shè)置一個(gè)名字艺配,以提高流程圖的可讀性酝掩。
活動(dòng)之間通過順序流(sequence flow)連接期虾,在流程圖中是一個(gè)有向箭頭喳坠。在執(zhí)行流程實(shí)例時(shí)壕鹉,執(zhí)行(execution)會(huì)從啟動(dòng)事件沿著順序流流向下一個(gè)活動(dòng)晾浴。
離開排他網(wǎng)關(guān)(帶有X的菱形)的順序流很特別:都以表達(dá)式(expression)的形式定義了條件(condition) 。當(dāng)流程實(shí)例的執(zhí)行到達(dá)這個(gè)網(wǎng)關(guān)時(shí)狸涌,會(huì)計(jì)算條件帕胆,并使用第一個(gè)計(jì)算為true的順序流懒豹。這就是排他的含義:只選擇一個(gè)。
這里用作條件的表達(dá)式為"{approved == true}的簡(jiǎn)寫淘这。變量’approved’被稱作流程變量(process variable)铝穷。
現(xiàn)在我們已經(jīng)有了流程BPMN 2.0 XML文件曙聂,下來需要將它'部署(deploy)'到引擎中断国。
4.部署流程定義
將流程定義部署至Flowable引擎稳衬,需要使用RepositoryService薄疚,其可以從ProcessEngine對(duì)象獲取街夭。使用RepositoryService,可以通過XML文件的路徑創(chuàng)建一個(gè)新的部署(Deployment)檐什,并調(diào)用deploy()方法實(shí)際執(zhí)行:
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("holiday-request.bpmn20.xml")
.deploy();
我們現(xiàn)在可以通過API查詢驗(yàn)證流程定義已經(jīng)部署在引擎中乃正。通過RepositoryService創(chuàng)建的ProcessDefinitionQuery對(duì)象實(shí)現(xiàn)。
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.singleResult();
System.out.println("Found process definition : " + processDefinition.getName());
5.啟動(dòng)流程實(shí)例
現(xiàn)在已經(jīng)在流程引擎中部署了流程定義名党,因此可以使用這個(gè)流程定義作為“藍(lán)圖”啟動(dòng)流程實(shí)例传睹。
要啟動(dòng)流程實(shí)例欧啤,需要提供一些初始化流程變量。一般來說倒慧,可以通過呈現(xiàn)給用戶的表單纫谅,或者在流程由其他系統(tǒng)自動(dòng)觸發(fā)時(shí)通過REST API照激,來獲取這些變量俩垃。在這個(gè)例子里口柳,我們簡(jiǎn)化為使用java.util.Scanner類在命令行輸入一些數(shù)據(jù):
Scanner scanner= new Scanner(System.in);
System.out.println("Who are you?");
String employee = scanner.nextLine();
System.out.println("How many holidays do you want to request?");
Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());
System.out.println("Why do you need them?");
String description = scanner.nextLine();
接下來,我們使用RuntimeService啟動(dòng)一個(gè)流程實(shí)例望艺。收集的數(shù)據(jù)作為一個(gè)java.util.Map實(shí)例傳遞找默,其中的鍵就是之后用于獲取變量的標(biāo)識(shí)符惩激。這個(gè)流程實(shí)例使用key啟動(dòng)。這個(gè)key就是BPMN 2.0 XML文件中設(shè)置的id屬性骡技,在這個(gè)例子里是holidayRequest哮兰。
<process id="holidayRequest" name="Holiday Request" isExecutable="true"></pre>
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n194" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", employee);
variables.put("nrOfHolidays", nrOfHolidays);
variables.put("description", description);
ProcessInstance processInstance =
runtimeService.startProcessInstanceByKey("holidayRequest", variables);
在流程實(shí)例啟動(dòng)后膏秫,會(huì)創(chuàng)建一個(gè)執(zhí)行(execution)窘哈,并將其放在啟動(dòng)事件上滚婉。從這里開始,這個(gè)執(zhí)行沿著順序流移動(dòng)到經(jīng)理審批的用戶任務(wù)帅刀,并執(zhí)行用戶任務(wù)行為让腹。這個(gè)行為將在數(shù)據(jù)庫中創(chuàng)建一個(gè)任務(wù),該任務(wù)可以之后使用查詢找到扣溺。用戶任務(wù)是一個(gè)等待狀態(tài)(wait state)骇窍,引擎會(huì)停止執(zhí)行,返回API調(diào)用處锥余。
6.查詢與完成任務(wù)
我們還沒有為用戶任務(wù)配置辦理人腹纳。我們想將第一個(gè)任務(wù)指派給"經(jīng)理(managers)"組,而第二個(gè)用戶任務(wù)指派給請(qǐng)假申請(qǐng)的提交人驱犹。因此需要為第一個(gè)任務(wù)添加candidateGroups屬性嘲恍,修改流程xml配置文件:
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
并如下所示為第二個(gè)任務(wù)添加assignee屬性。請(qǐng)注意我們沒有像上面的’managers’一樣使用靜態(tài)值雄驹,而是使用一個(gè)流程變量動(dòng)態(tài)指派佃牛。這個(gè)流程變量是在流程實(shí)例啟動(dòng)時(shí)傳遞的:
<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>
要獲得實(shí)際的任務(wù)列表,需要通過TaskService創(chuàng)建一個(gè)TaskQuery荠医。我們配置這個(gè)查詢只返回’managers’組的任務(wù):
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
System.out.println("You have " + tasks.size() + " tasks:");
for (int i=0; i<tasks.size(); i++) {
System.out.println((i+1) + ") " + tasks.get(i).getName());
}
可以使用任務(wù)Id獲取特定流程實(shí)例的變量吁脱,并在屏幕上顯示實(shí)際的申請(qǐng):
System.out.println("Which task would you like to complete?");
int taskIndex = Integer.valueOf(scanner.nextLine());
Task task = tasks.get(taskIndex - 1);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");
運(yùn)行結(jié)果像下面這樣:
經(jīng)理現(xiàn)在就可以完成任務(wù)了。在現(xiàn)實(shí)中彬向,這通常意味著由用戶提交一個(gè)表單兼贡。表單中的數(shù)據(jù)作為流程變量傳遞。在這里娃胆,我們?cè)谕瓿扇蝿?wù)時(shí)傳遞帶有’approved’變量(這個(gè)名字很重要废封,因?yàn)橹髸?huì)在順序流的條件中使用F蟆)的map來模擬:
boolean approved = scanner.nextLine().toLowerCase().equals("y");
variables = new HashMap<String, Object>();
variables.put("approved", approved);
taskService.complete(task.getId(), variables);
現(xiàn)在任務(wù)完成窟她,并會(huì)在離開排他網(wǎng)關(guān)的兩條路徑中穿剖,基于’approved’流程變量選擇一條昏苏。
7.實(shí)現(xiàn)JavaDelegate
代碼還缺了一塊:我們還沒有實(shí)現(xiàn)申請(qǐng)通過后執(zhí)行的自動(dòng)邏輯。在BPMN 2.0 XML中威沫,這是一個(gè)服務(wù)任務(wù)(service task):
<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="org.flowable.CallExternalSystemDelegate"/>
在現(xiàn)實(shí)中贤惯,這個(gè)邏輯可以做任何事情:向某個(gè)系統(tǒng)發(fā)起一個(gè)HTTP REST服務(wù)調(diào)用,或調(diào)用某個(gè)使用了好幾十年的系統(tǒng)中的遺留代碼棒掠。我們不會(huì)在這里實(shí)現(xiàn)實(shí)際的邏輯孵构,而只是簡(jiǎn)單的日志記錄流程。
創(chuàng)建一個(gè)新的JAVA類烟很,CallExternalSystemDelegate作為類名浦译。讓這個(gè)類實(shí)現(xiàn)org.flowable.engine.delegate.JavaDelegate接口棒假,并實(shí)現(xiàn)execute方法:
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
public class CallExternalSystemDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) {
System.out.println("Calling the external system for employee "
+ execution.getVariable("employee"));
}
}
當(dāng)執(zhí)行到達(dá)服務(wù)任務(wù)時(shí),會(huì)初始化并調(diào)用BPMN 2.0 XML中所引用的類精盅。
現(xiàn)在執(zhí)行這個(gè)例子的時(shí)候,就會(huì)顯示出日志信息谜酒,說明已經(jīng)執(zhí)行了自定義邏輯:
注意:如果出現(xiàn)“org.flowable.common.engine.api.FlowableException: couldn't instantiate class CallExternalSystemDelegate”的報(bào)錯(cuò)叹俏,說明包路徑未指定正確,在xml文件中僻族,根據(jù)自己CallExternalSystemDelegate類的實(shí)際路徑修改:
<serviceTask id="externalSystemCall" name="Enter holidays in external system" flowable:class="CallExternalSystemDelegate"/>
8.使用歷史數(shù)據(jù)
Flowable可以自動(dòng)存儲(chǔ)所有流程實(shí)例的數(shù)據(jù)粘驰。
例如,如果希望顯示流程實(shí)例已經(jīng)執(zhí)行的時(shí)間述么,就可以從ProcessEngine獲取HistoryService蝌数,并創(chuàng)建歷史活動(dòng)(historical activities)的查詢。在下面的代碼片段中度秘,可以看到我們添加了一些額外的過濾條件:
只選擇一個(gè)特定流程實(shí)例的活動(dòng)
只選擇已完成的活動(dòng)
結(jié)果按照結(jié)束時(shí)間排序顶伞,代表其執(zhí)行順序。
HistoryService historyService = processEngine.getHistoryService();
List<HistoricActivityInstance> activities =
historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstance.getId())
.finished()
.orderByHistoricActivityInstanceEndTime().asc()
.list();
for (HistoricActivityInstance activity : activities) {
System.out.println(activity.getActivityId() + " took "
+ activity.getDurationInMillis() + " milliseconds");
}
再次運(yùn)行例子剑梳,可以看到控制臺(tái)中顯示:
9.小結(jié)
這個(gè)教程介紹了很多Flowable與BPMN 2.0的概念與術(shù)語唆貌,也展示了如何編程使用Flowable API。