小伙伴们知道松哥最近在录TienChin项目视频,这个项目会用到工作流,为了帮助小伙伴们更好的理解这个项目,松哥最近会出几篇文章和大伙聊一聊工作流flowable的使用,算是给TienChin项目的第一个铺垫,当然,在TienChin项目的系列视频中,我也会和大家详细聊一聊flowable流程引擎的使用。 今天我就先写一个简单的请假流程,让小伙伴们对flowable先有一个直观的认知。1。效果展示 在正式开搞之前,我先来给小伙伴们看下我们今天要完成的效果。 简单起见,我这里并没有引入用户、角色等概念,涉及到用户的地方都是手动输入,在后续的文章中我会继续结合SpringSecurity来和大家展示引入用户之后的情况。 我们先来看看请假页面: 员工可以在这个页面输入姓名,请假天数以及请假理由等,然后点击按钮提交一个请假申请。 当员工提交请假申请之后,这个请假申请默认是由经理来处理的,此时经理登录之后,就可以看到员工提交上来的请求: 经理此时可以选择批准或者拒绝。无论是批准还是拒绝,都可以通过短信或者邮件等告知员工。 对于员工来说,也可以在一个页面查询自己请假流程的最终情况: 可能有小伙伴已经注意到了,我们这里所有涉及到用户名的地方,都需要手动输入。这是因为我为了让这个案例足够简单,暂时没有引入SpringSecurity,只是单纯的和大家分享Flowable的用法,等小伙伴们通过这篇文章掌握了Flowable的基本用法之后,下篇文章我会和大家分享如何结合具体的用户来使用。2。工程创建 我就直接来和小伙伴们展示SpringBoot中flowable的用法了。 首先我们创建一个SpringBoot项目,创建的时候引入Web和MySQL驱动依赖即可,项目创建成功之后,再引入flowable依赖,最终的依赖文件如下:dependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependencydependencygroupIdorg。flowablegroupIdflowablespringbootstarterartifactIdversion6。7。2versiondependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIdscoperuntimescopedependency 项目创建成功之后,首先需要我们在application。properties中配置一下数据库连接信息,如下:spring。datasource。usernamerootspring。datasource。password123spring。datasource。urljdbc:mysql:flowable02?serverTimezoneAsiaShanghaiuseSSLfalsenullCatalogMeansCurrenttrue 配置完成之后,当SpringBoot项目第一次启动的时候,会自动创建出来对应的表和需要的数据。 同时,SpringBoot项目也会自动创建并暴露Flowable中的ProcessEngine、CmmnEngine、DmnEngine、FormEngine、ContentEngine及IdmEngine等Bean。 并且所有的Flowable服务都暴露为SpringBean。例如RuntimeService、TaskService、HistoryService等等服务,我们都可以在需要使用的时候,直接注入就可以使用了。 同时:resourcesprocesses目录下的任何BPMN2。0流程定义都会被自动部署,所以在SpringBoot项目中,我们只需要将自己的流程文件放对位置即可,剩下的事情就会自动完成。cases目录下的任何CMMN1。1事例都会被自动部署。forms目录下的任何Form定义都会被自动部署。3。流程图分析 今天这个例子比较简单,就是一个请假流程,我暂时先不跟小伙伴们去扯画流程图的事,咱们直接用一个官网现成的请假流程图: 我们先来简单分析一下这张图:最左侧的圆圈叫做启动事件(startevent),这表示一个流程实例的起点。一个流程启动之后,首先到达第一个有用户图标的矩形中,这个矩形称为一个UserTask,在这个UserTask中,经理可以选择批准亦或者拒绝。UserTask的下一步是一个菱形,这个称作排他网关(ExclusiveGateway),这个会将请求路由到不同的地方。先说批准,如果在第一个矩形中,经理选择了批准,那么就会进入到一个带有齿轮图标的矩形中,在这个矩形中我们我们可以额外做一些事情,然后又会调用到一个UserTask,最终完成整个流程。如果经理选择了拒绝,则会进入到下面的发邮件的矩形中,在这个中我们可以给员工发送一个通知,告知他请假没有通过。当系统走到最右边的圆圈之后,就表示这个流程执行结束了。 这个流程图对应的XML文件位于srcmainresourcesprocessesholidayrequest。bpmn20。xml位置,其内容如下:?xmlversion1。0encodingUTF8?definitionsxmlnshttp:www。omg。orgspecBPMN20100524MODELxmlns:xsihttp:www。w3。org2001XMLSchemainstancexmlns:flowablehttp:flowable。orgbpmntypeLanguagehttp:www。w3。org2001XMLSchemaexpressionLanguagehttp:www。w3。org1999XPathtargetNamespacehttp:www。flowable。orgprocessdefprocessidholidayRequestnameHolidayRequestisExecutabletruestartEventidstartEventsequenceFlowsourceRefstartEventtargetRefapproveTaskuserTaskidapproveTasknameApproveorrejectrequestflowable:candidateGroupsmanagerssequenceFlowsourceRefapproveTasktargetRefdecisionexclusiveGatewayiddecisionsequenceFlowsourceRefdecisiontargetRefexternalSystemCallconditionExpressionxsi:typetFormalExpression!〔CDATA〔{approved}〕〕conditionExpressionsequenceFlowsequenceFlowsourceRefdecisiontargetRefrejectLeaveconditionExpressionxsi:typetFormalExpression!〔CDATA〔{!approved}〕〕conditionExpressionsequenceFlowserviceTaskidexternalSystemCallnameEnterholidaysinexternalsystemflowable:classorg。javaboy。flowable02。flowable。ApprovesequenceFlowsourceRefexternalSystemCalltargetRefholidayApprovedTaskuserTaskidholidayApprovedTaskflowable:assignee{employee}nameHolidayapprovedsequenceFlowsourceRefholidayApprovedTasktargetRefapproveEndserviceTaskidrejectLeavenameSendoutrejectionemailflowable:classorg。javaboy。flowable02。flowable。RejectsequenceFlowsourceRefrejectLeavetargetRefrejectEndendEventidapproveEndendEventidrejectEndprocessdefinitions 很多想学习流程引擎的小伙伴都会被这个XML文件劝退,但是!!! 如果你愿意静下心来认真阅读这个XML文件,你会发现流程引擎原来如此简单! 我们来挨个看下这里的每一个节点:process:这表示一个流程,例如本文和大家分享的请假就是一个流程。startEvent:这表示流程的开始,这就是一个开始事件。userTask:这就是一个具体的流程节点了,flowable:candidateGroups属性表示这个节点该由哪个用户组中的用户来处理。sequenceFlow:这就是连接各个流程节点之间的线条,这个里边一般有两个属性,sourceRef和targetRef,前者表示线条的起点,后者表示线条的终点。exclusiveGateway:表示一个排他性网关,也就是那个菱形选择框。从排他性网关出来的线条有两个,大家注意看上面的代码,这两个线条中都涉及到一个变量approved,如果这个变量为true,则targeRef就是externalSystemC如果这个变量为false,则targetRef就是rejectLeave。serviceTask:这就是我们定义的一个具体的外部服务,如果在整个流程执行的过程中,你有一些需要自己完成的事情,那么可以通过serviceTask来实现,这个节点会有一个flowable:class属性,这个属性的值就是一个自定义类。另外,上文中部分节点中还涉及到变量{},这个变量是在流程执行的过程中传入进来的。 总而言之,只要小伙伴们静下心来认真阅读一下上面的XML,你会发现SoEasy!4。请假申请 好了,接下来我们就来看一个具体的请假申请。由于请假流程只要放对位置,就会自动加载,所以我们并不需要手动加载请假流程,直接开始一个请假申请流程即可。4。1服务端接口 首先我们需要一个实体类来接受前端传来的请假参数:用户名、请假天数以及请假理由:publicclassAskForLeaveVO{privateSprivateIprivateS省略gettersetter} 再拿出祖传的RespBean,以便响应数据方便一些:publicclassRespBean{privateIprivateSprivateOpublicstaticRespBeanok(Stringmsg,Objectdata){returnnewRespBean(200,msg,data);}publicstaticRespBeanok(Stringmsg){returnnewRespBean(200,msg,null);}publicstaticRespBeanerror(Stringmsg,Objectdata){returnnewRespBean(500,msg,data);}publicstaticRespBeanerror(Stringmsg){returnnewRespBean(500,msg,null);}privateRespBean(){}privateRespBean(Integerstatus,Stringmsg,Objectdata){this。this。this。}省略gettersetter} 接下来我们提供一个处理请假申请的接口:RestControllerpublicclassAskForLeaveController{AutowiredAskForLeaveServiceaskForLeaveSPostMapping(askforleave)publicRespBeanaskForLeave(RequestBodyAskForLeaveVOaskForLeaveVO){returnaskForLeaveService。askForLeave(askForLeaveVO);}} 核心逻辑在AskForLeaveService中,来继续看:ServicepublicclassAskForLeaveService{AutowiredRuntimeServiceruntimeSTransactionalpublicRespBeanaskForLeave(AskForLeaveVOaskForLeaveVO){MapString,ObjectvariablesnewHashMap();variables。put(name,askForLeaveVO。getName());variables。put(days,askForLeaveVO。getDays());variables。put(reason,askForLeaveVO。getReason());try{runtimeService。startProcessInstanceByKey(holidayRequest,askForLeaveVO。getName(),variables);returnRespBean。ok(已提交请假申请);}catch(Exceptione){e。printStackTrace();}returnRespBean。error(提交申请失败);}} 小伙伴们看一下,在提交请假申请的时候,分别传入了name、days以及reason三个参数,我们将这三个参数放入到一个Map中,然后通过RuntimeServicestartProcessInstanceByKey方法来开启一个流程,开启流程的时候一共传入了三个参数:第一个参数表示流程引擎的名字,这就是我们刚才在流程的XML文件中定义的名字。第二个参数表示当前这个流程的key,我用了申请人的名字,将来我们可以通过申请人的名字查询这个人曾经提交的所有申请流程。第三个参数就是我们的变量了。 好了,这服务端就写好了。4。2前端页面 接下来我们来开发前端页面。 前端我使用VueElementUIAxios,咱们这个案例比较简单,就没有必要搭建单页面了,直接用普通的HTML就行了。另外,Vue我是用了Vue3:!DOCTYPEhtmlhtmllangenheadmetacharsetUTF8titleTitletitle!Importstylelinkrelstylesheethrefunpkg。comelementplusdistindex。css!Importcomponentlibraryheadbodyh1开始一个请假流程h1tabletrtd请输入姓名:tdtdelinputtypetextvmodelafl。nametdtrtrtd请输入请假天数:tdtdelinputtypetextvmodelafl。daystdtrtrtd请输入请假理由:tdtdelinputtypetextvmodelafl。reasontdtrtableelbuttontypeprimaryclicksubmit提交请假申请elbuttonbodyhtml 这个页面有几个需要注意的点:通过Vue。createApp来创建一个Vue实例,这跟以前Vue2中直接new一个Vue实例不一样。在最下面,通过use来配置ElementPlus插件,这个跟Vue2也不一样。在Vue2中,如果我们单纯的在HTML页面中引用ElementUI并不需要这个步骤。剩下的东西就比较简单了,上面先引入Vue3、Axios以及ElementPlus,然后三个输入框,点击按钮提交请求,参数就是三个输入框中的数据,提交成功或者失败,分别弹个框出来提示一下就行了。 好啦,这就写好了。 然而,提交完成后,没有一个直观的展示,虽然前端提示说提交成功了,但是究竟成功没,还得眼见为实。5。任务展示 好了,接下来我们要做的事情就是把用户提交的流程展示出来。 按理说,比如经理登录成功之后,系统页面就自动展示出来经理需要审批的流程,但是我们当前这个例子为了简单,就没有登录这个操作了,需要需要用户将来在网页上选一下自己的身份,接下来就会展示出这个身份所对应的需要操作的流程。 我们来看任务接口:GetMapping(list)publicRespBeanleaveList(Stringidentity){returnaskForLeaveService。leaveList(identity);} 这个请求参数identity就表示当前用户的身份(本来应该是登录后自动获取,但是因为我们目前没有登录,所以这个参数是由前端传递过来)。来继续看askForLeaveService中的方法:ServicepublicclassAskForLeaveService{AutowiredTaskServicetaskSpublicRespBeanleaveList(Stringidentity){ListTasktaskstaskService。createTaskQuery()。taskCandidateGroup(identity)。list();ListMapString,ObjectlistnewArrayList();for(inti0;itasks。size();i){Tasktasktasks。get(i);MapString,ObjectvariablestaskService。getVariables(task。getId());variables。put(id,task。getId());list。add(variables);}returnRespBean。ok(加载成功,list);}} Task就是流程中要做的每一件事情,我们首先通过TaskService,查询出来这个用户需要处理的任务,例如前端前传来的是managers,那么这里就是查询所有需要由managers用户组处理的任务。 这段代码要结合流程图一起来理解,小伙伴们回顾下我们流程图中有如下一句:userTaskidapproveTasknameApproveorrejectrequestflowable:candidateGroupsmanagers 这意思就是说这个userTask是由managers这个组中的用户来处理,所以上面Java代码中的查询就是查询managers这个组中的用户需要审批的任务。 我们将所有需要审批的任务查询出来后,通过taskId可以进一步查询到这个任务中当时传入的各种变量,我们将这些数据封装成一个对象,并最终返回到前端。 最后,我们再来看下前端页面:!DOCTYPEhtmlhtmllangenheadmetacharsetUTF8titleTitletitle!Importstylelinkrelstylesheethrefunpkg。comelementplusdistindex。css!Importcomponentlibraryheadbody请选择你的身份:elselectnameidvmodelidentitychangeinitTaskseloption:valueidenvfor(iden,index)inidentities:keyindex:labelideneloptionelselectelbuttontypeprimaryclickinitTasks刷新一下elbuttoneltableborderstrip:datataskseltablecolumnpropnamelabel姓名eltablecolumneltablecolumnpropdayslabel请假天数eltablecolumneltablecolumnpropreasonlabel请假原因eltablecolumneltablecolumnlable操作templatedefaultscopeelbuttontypeprimaryclickapproveOrReject(scope。row。id,true,scope。row。name)批准elbuttonelbuttontypedangerclickapproveOrReject(scope。row。id,false,scope。row。name)拒绝elbuttontemplateeltablecolumneltablebodyhtml 大家看到,首先有一个下拉框,我们在这个下拉框中来选择用户的身份。选择完成后,触发initTasks方法,然后在这个方法中,发起网络请求,最终将请求结果渲染出来。 最终效果如下: 当然用户也可以点击刷新按钮,刷新列表。 这样,当第五小节中,员工提交了一个请假审批之后,我们在这个列表中就可以查看到员工提交的请假审批了(在流程图中,我们直接设置了用户的请假审批固定提交给managers,在后续的文章中,松哥会教大家如何把这个提交的目标用户变成一个动态的)。6。请假审批 接下来经理就可以选择批准或者是拒绝这请假了。 首先我们封装一个实体类用来接受前端传来的请求:publicclassApproveRejectVO{privateStringtaskId;privateBprivateS省略gettersetter} 参数都好理解,approve为true表示申请通过,false表示申请被拒绝。 接下来我们来看接口:PostMapping(handler)publicRespBeanaskForLeaveHandler(RequestBodyApproveRejectVOapproveRejectVO){returnaskForLeaveService。askForLeaveHandler(approveRejectVO);} 看具体的askForLeaveHandler方法:ServicepublicclassAskForLeaveService{AutowiredTaskServicetaskSpublicRespBeanaskForLeaveHandler(ApproveRejectVOapproveRejectVO){try{booleanapprovedapproveRejectVO。getApprove();MapString,ObjectvariablesnewHashMapString,Object();variables。put(approved,approved);variables。put(employee,approveRejectVO。getName());TasktasktaskService。createTaskQuery()。taskId(approveRejectVO。getTaskId())。singleResult();taskService。complete(task。getId(),variables);if(approved){如果是同意,还需要继续走一步TaskttaskService。createTaskQuery()。processInstanceId(task。getProcessInstanceId())。singleResult();taskService。complete(t。getId());}returnRespBean。ok(操作成功);}catch(Exceptione){e。printStackTrace();}returnRespBean。error(操作失败);}} 大家注意这个审批流程:审批时需要两个参数,approved和employee,approved为true,就会自动进入到审批通过的流程中,approved为false则会自动进入到拒绝流程中。通过taskService,结合taskId,从流程中查询出对应的task,然后调用taskService。complete方法传入taskId和变量,以使流程向下走。小伙伴们再回顾一下我们前面的流程图,如果请求被批准备了,那么在执行完自定义的Approve逻辑后,就会进入到Holidayapproved这个userTask中,注意此时并不会继续向下走了(还差一步到结束事件);如果是请求拒绝,则在执行完自定义的Reject逻辑后,就进入到结束事件了,这个流程就结束了。针对第三条,所以代码中我们还需要额外再加一步,如果是approved为true,那么就再从当前流程中查询出来需要执行的task,再调用complete继续走一步,此时就到了结束事件了,这个流程就结束了。注意这次的查询是根据当前流程的ID查询的,一个流程就是一条线,这条线上有很多Task,我们可以从Task中获取到流程的ID。 好啦,接口就写好了。 当然,这里还涉及到两个自定义的逻辑,就是批准或者拒绝之后的自定义逻辑,这个其实很好写,如下:publicclassApproveimplementsJavaDelegate{Overridepublicvoidexecute(DelegateExecutionexecution){System。out。println(申请通过:execution。getVariables());}} 我们自定义类实现JavaDelegate接口即可,然后我们在execute方法中做自己想要做的事情即可,execution中有这个流程中的所有变量。我们可以在这里发邮件(公众号江南一点雨后台回复666有发邮件教程)、发短信等等。Reject的定义方式也是类似的。这些自定义类写好之后,将来配置到流程图中即可(可查看上文的流程图)。 最后再来看看前端提交方法就简单了(页面源码上文已经列出):approveOrReject(taskId,approve,name){axios。post(handler,{taskId:taskId,approve:approve,name:name})。then(function(response){this。initTasks();})。catch(function(error){console。log(error);});} 这就一个普通的Ajax请求,批准的话第二个参数就为true,拒绝的话第二个参数就为false。7。结果查询 最后,每个用户都可以查看自己曾经的申请记录。本来这个登录之后就可以展示了,但是因为我们没有登录,所以这里也是需要手动输入查询的用户,然后根据用户名查询这个用户的历史记录,我们先来看查询接口:GetMapping(search)publicRespBeansearchResult(Stringname){returnaskForLeaveService。searchResult(name);} 参数就是要查询的用户名。具体的查询流程如下:publicRespBeansearchResult(Stringname){ListHistoryInfohistoryInfosnewArrayList();ListHistoricProcessInstancehistoricProcessInstanceshistoryService。createHistoricProcessInstanceQuery()。processInstanceBusinessKey(name)。finished()。orderByProcessInstanceEndTime()。desc()。list();for(HistoricProcessInstancehistoricProcessInstance:historicProcessInstances){HistoryInfohistoryInfonewHistoryInfo();DatestartTimehistoricProcessInstance。getStartTime();DateendTimehistoricProcessInstance。getEndTime();ListHistoricVariableInstancehistoricVariableInstanceshistoryService。createHistoricVariableInstanceQuery()。processInstanceId(historicProcessInstance。getId())。list();for(HistoricVariableInstancehistoricVariableInstance:historicVariableInstances){StringvariableNamehistoricVariableInstance。getVariableName();ObjectvaluehistoricVariableInstance。getValue();if(reason。equals(variableName)){historyInfo。setReason((String)value);}elseif(days。equals(variableName)){historyInfo。setDays(Integer。parseInt(value。toString()));}elseif(approved。equals(variableName)){historyInfo。setStatus((Boolean)value);}elseif(name。equals(variableName)){historyInfo。setName((String)value);}}historyInfo。setStartTime(startTime);historyInfo。setEndTime(endTime);historyInfos。add(historyInfo);}returnRespBean。ok(ok,historyInfos);}我们当时在开启流程的时候,传入了一个参数key,这里就是再次通过这个key,也就是用户名去查询历史流程,查询的时候还加上了finished方法,这个表示要查询的流程必须是执行完毕的流程,对于没有执行完毕的流程,这里不查询,查完之后,按照流程最后的处理时间进行排序。遍历第一步的查询结果,从HistoricProcessInstance中提取出每一个流程的详细信息,并存入到集合中,并最终返回。这里涉及到两个历史数据查询,createHistoricProcessInstanceQuery用来查询历史流程,而createHistoricVariableInstanceQuery则主要是用来查询流程变量的。 最后,前端通过表格展示这个数据即可:!DOCTYPEhtmlhtmllangenheadmetacharsetUTF8titleTitletitle!Importstylelinkrelstylesheethrefunpkg。comelementplusdistindex。css!Importcomponentlibraryheadbodyelinputvmodelnamestylewidth:300pxplaceholder请输入用户名elinputelbuttontypeprimaryclicksearch查询elbuttoneltableborderstrip:datahistoryInfoseltablecolumnpropnamelabel姓名eltablecolumneltablecolumnpropstartTimelabel提交时间eltablecolumneltablecolumnpropendTimelabel审批时间eltablecolumneltablecolumnpropreasonlabel事由eltablecolumneltablecolumnpropdayslabel天数eltablecolumneltablecolumnlabel状态templatedefaultscopeeltagtypesuccessvifscope。row。status已通过eltageltagtypedangervelse已拒绝eltagtemplateeltablecolumneltablebodyhtml 这个都是一些常规操作,我就不多说了,最终展示效果如下: 8。小结 好啦,一个简单的请假流程,让大家对Flowable的玩法有一个基本的认知,下篇文章松哥来和大家继续完善本文。Flowable的视频将会出现在TienChin项目中,大家不要错过哦:TienChin项目配套视频来啦。