管理流程定义

设计流程定义文档

bpmn文件

设置方式可以直接使用插件图形化界面进行设置
为某任务节点指定任务执行者
保存后的BPMN文件可以使用XML编辑器打开
BPMN 2.0根节点是definitions节点。 这个元素中,可以定义多个流程定义(不过我们建议每个文件只包含一个流程定义, 可以简化开发过程中的维护难度)。 一个空的流程定义看起来像下面这样。注意,definitions元素 最少也要包含xmlns 和 targetNamespace的声明。 targetNamespace可以是任意值,它用来对流程实例进行分类。
<?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:activiti="http://activiti.org/bpmn" 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" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://activiti.liuxun/test"><!-- 流程定义部分 -->  <process id="myProcess" name="My First process" isExecutable="true"><startEvent id="startevent1" name="Start"></startEvent><userTask id="usertask1" name="员工申请请假" activiti:assignee="员工"></userTask><sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow><userTask id="usertask2" name="部门经理审批" activiti:assignee="经理"></userTask><sequenceFlow id="flow2" name="提交申请" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow><userTask id="usertask3" name="总经理审批" activiti:assignee="总经理"></userTask><sequenceFlow id="flow3" name="经理审批" sourceRef="usertask2" targetRef="usertask3"></sequenceFlow><endEvent id="endevent1" name="End"></endEvent><sequenceFlow id="flow4" sourceRef="usertask3" targetRef="endevent1"></sequenceFlow></process><!-- BPMN绘图规范定义部分(用来描述节点图标的大小和坐标) --><bpmndi:BPMNDiagram id="BPMNDiagram_myProcess"><bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess"><bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1"><omgdc:Bounds height="35.0" width="35.0" x="40.0" y="138.0"></omgdc:Bounds>......</bpmndi:BPMNPlane>......</bpmndi:BPMNDiagram>
</definitions>
说明:流程定义文档由两部分组成
(1) bpmn文件
流程规则文件。在部署后,每次系统启动时都会被解析,把内容封装成流程定义放入项目缓存中。Activiti框架结合XML文件自动管理流程。
(2) 展示流程图的图片
在系统里需要展示流程进展的图片

部署流程定义

部署流程定义也可以认为是增加流程定义

/*** 1.部署流程定义*/
@Test
public void deploy() {// 创建流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 创建部署环境配置对象DeploymentBuilder deploymentBuilder = processEngine.getRepositoryService().createDeployment();// 部署流程// 方式一:读取单个的流程定义文件Deployment deployment = deploymentBuilder.name("测试") //设置部署流程的名称.addClasspathResource("activiti/lx/processdefination/MyProcess.bpmn") //设置流程文件.addClasspathResource("activiti/lx/processdefination/MyProcess.png") //设置流程文件.deploy(); // 部署System.out.println("部署ID: "+deployment.getId());
}
说明

(1) 通过getDefaultProcessEngine() 首先获得默认的流程引擎,在创建时会自动加载classpath下的activiti.cfg.xml或activiti-context.xml

(2) 通过流程引擎获取一个RepositoryService对象(仓库服务对象)
(3) 由仓库的服务对象可以获取一个部署环境的配置对象,用来封装部署环境的相关配置。
(4) 部署环境的配置对象(deploymentBuilder) 支持链式编程,在部署配置对象中设置显示名,上传规则文件相对的classpath的地址
(5) 部署,也是往数据库中存储流程定义的过程。
(6) 部署流程定义在数据库中主要操作三张表
a) ACT_RE_DEPLOYMENT 存放流程定义的显示名和部署时间,每部署一次增加一条记录。
b) ACT_RE_PROCDEF 存放流程定义的属性信息,部署每个新的流程定义都会在这张表中插入一条记录。
c) ACT_RE_BYTEARRAY 
存放流程定义相关的部署信息。即流程定义文档的存放地。每部署一次就会增加两条记录,一条是关于bpmn规则文件的,另一条是图片的(如果部署时只指定了bpmn文件,activiti会在部署时解析bpmn文件内容自动生成流程图)。两个文件不是很大,都是以二进制的形式存储在数据库中。
此种方式只适合部署单个的流程,可以以Zip格式进行部署(将所有的流程规则文件与图片压缩成ZIP格式文件)
/*** 1.部署流程定义(部署zip格式文件)*/
@Test
public void deploy2() {// 创建流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 创建部署环境配置对象DeploymentBuilder deploymentBuilder = processEngine.getRepositoryService().createDeployment();// 部署流程ZipInputStream zipInputStream = new ZipInputStream(this.getClass().getClassLoader().getResourceAsStream("activiti/lx/processdefination/process.zip"));// 方式二:读取zip压缩文件Deployment deployment = deploymentBuilder.name("测试zip部署") //设置部署流程的名称.addZipInputStream(zipInputStream ).deploy();System.out.println("部署ID: "+deployment.getId());
}

查看流程定义

/*** 2. 查看流程规则的信息(流程定义)*/
@Test
public void view() {// 创建流程引擎对象ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取流程定义信息RepositoryService repositoryService = processEngine.getRepositoryService();ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery();// 添加过滤条件// definitionQuery.processDefinitionName("");definitionQuery.processDefinitionKey("myProcess");// 添加分页条件// definitionQuery.listPage(0, 10);// 添加排序条件// definitionQuery.orderByProcessDefinitionId();List<ProcessDefinition> list = definitionQuery.list();// 迭代效果查看流程定义for (ProcessDefinition pd : list) {System.out.print("id= "+pd.getId()+", ");System.out.print("name= "+pd.getName()+", ");System.out.print("key= "+pd.getKey()+", ");System.out.print("version= "+pd.getVersion()+", ");System.out.println("deploymentId= "+pd.getDeploymentId()+", ");}}

结果:


再部署一次的运行结果为:
说明:
(1) 首先获取流程引擎对象
(2) 然后通过流程引擎对象获取RepositoryService
(3)通过RepositoryService实例可以获取查询对象实例
(4) 根据查询实例可以设置过滤参数,设置查询以及排序条件,获得符合条件的流程定义列表。
(5)由运行结果可以看出:
a) Key和Name的值为:bpmn文件process节点的id和name的属性值

  <process id="myProcess" name="My First process" isExecutable="true">

b) key属性被用来区别不同的流程定义。
c) 带有特定key的流程定义第一次部署时,version为1。之后每次部署都会在当前最高版本号上加1
d) Id的值的生成规则为:{processDefinitionKey}:{processDefinitionVersion}:{generated-id}, 这里的generated-id是一个自动生成的唯一的数字
e) 重复部署一次,deploymentId的值以一定的形式变化
f) 流程定义(ProcessDefinition)在数据库中没有相应的表对应,只是从act_ge_bytearray表中取出相应的bpmn和png图片,并进行解析。

删除流程定义

删除部署到activiti中的流程定义
/***3.删除流程定义*/
@Test
public void  delDeploy() {// 创建流程引擎对象ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 设置要删除的部署的IDString deploymentId = "101";// 删除指定的部署信息,如果有关联信息则报错(例如启动了流程定义相关的流程实例)// processEngine.getRepositoryService().deleteDeployment(deploymentId );// 删除指定的部署信息,如果有关联信息则级联删除// 第二个参数cascade,代表是否级联删除processEngine.getRepositoryService().deleteDeployment(deploymentId, true);
}

说明:

(1) 因为删除的是流程定义,而流程定义的部署是属于仓库服务的,所以应该首先获取RespositoryService
(2) 如果该流程定义在没有正在运行的情况下,可以使用普通的删除方式。如果有关联的信息,可以使用第二种级联强制删除(删除所有关联的信息)。由于级联删除涉及的数据比较多,一般只开放给超级管理员使用。

获取流程定义文档的资源

查询流程定义文档。主要查看的是图片,用于显示流程使用。

/*** 4.获取流程定义中的资源文件(查看流程图片)* * @throws IOException*/
@Test
public void getResource() throws IOException {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();String deploymentId = "1";// 获取指定ID流程定义下的所有资源文件的名称列表List<String> names = processEngine.getRepositoryService().getDeploymentResourceNames(deploymentId);String resourceName = null;// 遍历资源文件名称列表for (String string : names) {// 获取'.png'结尾名称为流程图片名称if (string.endsWith(".png")) {resourceName = string;}}// 如果流程图片存在if (resourceName != null) {InputStream in = processEngine.getRepositoryService().getResourceAsStream(deploymentId, resourceName);// 指定拷贝目录File file = new File("/Users/liuxun/Downloads/" + resourceName);// 原始方式// OutputStream out = new FileOutputStream(file);// byte[] b = new byte[1024];// int len = 0;// while((len=in.read(b))!=-1) {// out.write(b, 0, len);// }// out.close();// 使用FileUtils文件操作工具类,将流程图片拷贝到指定目录下FileUtils.copyInputStreamToFile(in, file);}}

查看ACT_GE_BYTERRAY


运行结果:
1)deploymentId为流程部署ID
2)resourceName为act_ge_bytearray表中NAME_列的值
3)使用repositoryService的getDeploymentResourceNames方法可以获取指定部署下得所有文件的名称
4)使用repositoryService的getResourceAsStream方法传入部署ID和文件名称可以获取部署下指定名称文件的输入流
5)最后的有关IO流的操作,使用FileUtils工具的copyInputStreamToFile方法完成流程流程到文件的拷贝

查询最新版本的流程定义

/*** 获取最新版本的流程定义*/
@Test
public void getLatestVersion() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();ProcessDefinitionQuery query = processEngine.getRepositoryService().createProcessDefinitionQuery();query.orderByProcessDefinitionId().asc();List<ProcessDefinition> list = query.list();HashMap<String, ProcessDefinition> map = new HashMap<String, ProcessDefinition>();for (ProcessDefinition pd : list) {map.put(pd.getKey(), pd);}ArrayList<ProcessDefinition> lastList = new ArrayList<>(map.values());for (ProcessDefinition processDefinition : lastList) {System.out.println(processDefinition.getName()+" "+processDefinition.getVersion());}
}

流程实例管理

启动流程实例

在完成流程定义部署后,就可以启动流程实例了
// 启动流程实例
@Test
public void startProcess() {// 创建流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取流程引擎服务对象RuntimeService runtimeService = processEngine.getRuntimeService();// 启动流程,返回流程实例对象ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess");// 显示相关信息System.out.print("pid= "+processInstance.getId()); //流程实例IDSystem.out.println("activityId= "+processInstance.getActivityId());
}
说明:
(1) 结果为:
(2) 操作数据库的ACT_RU_EXECUTION表,如果是用户任务节点,同时也会在ACT_RU_TASK中添加一条记录
注意:按照key启动流程 会默认根据最新版本的流程定义规则创建流程实例
ACT_RU_EXECUTION

ACT_RU_TASK

查询任务

在activiti任务中,主要分为两大类:
1.确切指定了办理者的任务,这个任务将成为指定者的私有任务
2.无法指定具体的某一个人来办理的任务,可以把任务分配给几个人或者一到多个小组,让这个范围内的用户可以选择性(如有空余时间时)来办理这类任务。

查询指定用户的代办任务

对指定用户的未完成的个人任务执行查询(由某一个人负责办理 在任务列表中通过assignee字段指定)
/*** 2.1 查看指定用户的代办任务*/
@Test
public void findUnfinishedTask() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取个人的代办信息TaskService taskService = processEngine.getTaskService();// 创建流程任务查询对象TaskQuery taskQuery = taskService.createTaskQuery();String assignee = "员工";// 添加过滤条件taskQuery.taskAssignee(assignee);// 添加分页条件taskQuery.listPage(0, 10);// 添加过滤条件taskQuery.orderByTaskCreateTime().desc();// 执行查询List<Task> list = taskQuery.list();System.out.println("===============【"+assignee+"】的个人任务列表===============");// 迭代结果,查看个人任务for (Task task : list) {System.out.print("id="+task.getId()+","); //获取任务的IDSystem.out.print("name="+task.getName()+",");//获取任务的名称System.out.print("assign="+task.getAssignee()+",");//查询任务的代办人System.out.print("createTime="+task.getCreateTime()+",");//查询任务的创建时间System.out.println("executionId="+task.getExecutionId());//获取流程执行对象的ID}
}

运行结果:


说明:
1)因为是任务查询,所以从processEngine中应该得到TaskService
2)使用TaskService获取到任务查询对象TaskQuery
3)为查询对象添加查询过滤条件,使用taskAssignee指定任务的候选者(即查询指定用户的代办任务),添加分页排序等过滤条件
4)调用list方法执行查询,返回办理者为指定用户的任务列表
5)任务ID、名称、办理人、创建时间可以从act_ru_task表中查到。
6)Execution与ProcessInstance的关系与分支有关。在这种单线流程中,ProcessInstance相当于Execution
7)如果assignee属性为部门经理,结果为空。因为现在流程只到了”填写请假申请”阶段,后面的任务还没有执行,即在数据库中没有部门经理可以办理的任务,所以查询不到。
8)一个Task节点和Execution节点是1对1的情况,在task对象中使用Execution_来标示他们之间的关系
9)任务ID在数据库表act_ru_task中对应“ID_”列

查询指定用户的可接任务(公共任务)

对指定用户的可接收的公共任务执行查询
/*** 2.2 查看指定用户的可接任务(公共任务)*/
@Test
public void findCanTakeTask() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取个人的代办信息TaskService taskService = processEngine.getTaskService();// 创建流程任务查询对象TaskQuery taskQuery = taskService.createTaskQuery();String candidateUser = "员工1";// 添加过滤条件taskQuery.taskCandidateUser(candidateUser );// 添加分页条件taskQuery.listPage(0, 10);// 添加过滤条件taskQuery.orderByTaskCreateTime().desc();// 执行查询List<Task> list = taskQuery.list();System.out.println("===============【"+candidateUser+"】的可接收任务列表===============");// 迭代结果,查看个人任务for (Task task : list) {System.out.print("id="+task.getId()+","); //获取任务的IDSystem.out.print("name="+task.getName()+",");//获取任务的名称System.out.print("assign="+task.getAssignee()+",");//查询任务的代办人System.out.print("createTime="+task.getCreateTime()+",");//查询任务的创建时间System.out.println("executionId="+task.getExecutionId());//获取流程执行对象的ID}
}

1.前面步骤类似,查询任务首先使用TaskService创建TaskQuery对象
2.在查询对象上,添加taskCandidateUser过滤条件,代表过滤任务候 选者为自己的任务
3.调用list方法返回指定用户的可接任务列表
4.所有公共任务的assignee属性为空

认领任务

通常一个公共任务都有一个以上的候选者,用户想要办理它应该先进行认领任务操作,即把自己变成任务的拥有者
/*** 3.认领任务*/
@Test
public void takeTask() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 接手的任务IDString taskId = "304";// 接手任务的员工IDString userId = "员工1";// 让"员工1"认领ID为"304"的任务processEngine.getTaskService().claim(taskId, userId);
}

说明:

(1) 任务相关操作,首先获取TaskService
(2) 确定被认领的任务ID和认领人的ID
(3) 调用taskService的claim(认领)方法,把公共任务变成指定用户的私有任务

退回任务

退回任务是针对公共任务来说的,在公共任务被某个候选人领取后变成了私人任务,这时如果处于某种原因,候选人可以将领取到的任务退回变成公共任务。
/*** 3.2退回任务(将个人任务变成公共任务)* */
@Test
public void backTask() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();String taskId = "304";String userId = "员工1";processEngine.getTaskService().setAssignee(taskId, userId);
}

办理任务

指定任务ID,完成该任务

/*** 4。办理任务(完成任务后,让流程往后移)*/
@Test
public void completeTask() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 指定任务IDString taskId = "104";// 完成任务processEngine.getTaskService().complete(taskId );
}

依次执行 查看任务表



流程执行完毕后,任务表里与该流程实例相关的记录就会被删除
说明:
1)是办理任务,所以从ProcessEngine得到的是TaskService。
2)当执行完这段代码,再以员工的身份去执行查询的时候,会发现这个时候已经没有数据了。
3)对于执行完的任务,activiti将从act_ru_task表中删除该任务,下一个任务会被插入进来。
4)以”经理”的身份进行查询,可以查到结果。因为流程执行到经理审批这个节点了。
5)再执行办理任务代码,执行完以后以”经理”身份进行查询,没有结果。
6)重复第3和4步直到流程执行完。

验证流程已经结束

在流程指定的过程中,创建的流程实例的ID在整个过程中都不会变,当流程结束后,流程实例将会被删除。
/*** 5. 验证流程是否结束*/
@Test
public void checkProcessEnded() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RuntimeService runtimeService = processEngine.getRuntimeService();ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery();// 通过流程定义ID获取到流程定义实例对象String procId = "101";ProcessInstance processInstance = processInstanceQuery.processDefinitionId(procId).singleResult();// 查看流程定义是否已经完成if (processInstance!= null) {System.out.println("当前活动ID为:"+processInstance.getActivityId());} else {System.out.println("ID为【"+procId+"】的流程实例已经结束");}
}

说明:

(1) 因为是流程实例查询,所以先获取runtimeService
(2) 创建流程查询对象,设置实例ID过滤参数

(3)由于一个流程实例ID只对应一个实例,使用singleResult执行查询返回一个唯一的结果,如果结果数量大于1,则抛出异常
(4)判断指定ID的实例是否存在,如果结果为空,则代表流程结束,实例已被删除

流程历史

虽然已完成的任务在act_ru_task和act_ru_execution表中都已被删除,但是这些数据还存在activiti的数据库中,作为历史改由HistoryService来管理。
历史是一个组件,它可以捕获发生在进程执行中的信息并永久的保存,与运行时数据不同的是,当流程实例运行完成之后它还会存在于数据库中。
在流程引擎配置对象中可以设置历史记录规则:

<!-- 配置流程引擎配置对象 --><bean id="processEngineConfiguration"class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration"><property name="jdbcDriver" value="com.mysql.jdbc.Driver" /><property name="jdbcUrl" value="jdbc:mysql:///activiti" /><property name="jdbcUsername" value="root" /><property name="jdbcPassword" value="root" /><property name="databaseSchemaUpdate" value="true" /><!-- Activiti默认提供了4种历史流程none:不保存任何历史记录,可以提高系统性能activity:保存所有的流程实例、任务、活动信息audit:也是activiti的默认级别,保存所有的流程实例、任务、活动、表单属性full:最完整的记录,除了包含audit级别的信息之外还能保存流程变量等信息--><property name="history" value="activity"/></bean>

由于数据库中保存着历史信息以及正在运行的流程实例信息,在实际项目中对已完成任务的查看频率远不及对代办和可接任务的查看,所以在activiti采用分开管理,把正在运行的交给runtimeService管理,而历史数据交给HistoryService来管理。
对已成为历史的数据主要进行查询操作,我们主要关心两种类型的历史数据:
HistoricProcessInstance 包含当前和已经结束的流程实例信息。
HistoricActivityInstance 包含一个活动(流程上的节点)的执行信息 。
HistoricTaskInstance 包含一个任务的相关信息

刚才运行完一个流程实例 查看历史表中有关的数据 如下图
ACT_HI_PROCINST
ACT_HI_ACTINST
ACT_HI_TASKINST

查看历史流程实例

查看用户按照某个流程规则执行了多少次流程
/*** 1. 查看历史流程实例* 查看系统一共按照某个规则执行了多少次流程*/
@Test
public void queryHistoryInstance() {// 创建流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();HistoryService historyService = processEngine.getHistoryService();// 创建流程实例查询对象HistoricProcessInstanceQuery instanceQuery = historyService.createHistoricProcessInstanceQuery();// 指定流程定义IDString processDefinitionId = "myProcess:1:4";// 添加过滤条件instanceQuery.processDefinitionId(processDefinitionId);// 排序条件instanceQuery.orderByProcessInstanceStartTime().desc();// 查询已经完成的实例instanceQuery.finished();// 分页查询instanceQuery.listPage(0, 10);// 执行查询List<HistoricProcessInstance> list = instanceQuery.list();for (HistoricProcessInstance hp : list) {System.out.print("ID: "+hp.getProcessDefinitionId()+", ");System.out.print("开始活动ID: "+hp.getStartActivityId()+", ");System.out.print("开始时间: "+hp.getStartTime()+", ");System.out.println("结束时间: "+hp.getEndTime()+", ");}
}


说明:

(1) 通常查询历史流程实例都需要指定一个过滤条件,指定processDefinitionId查看具体某一次部署所开启的流程或者指定processDefinitionKey查看某个规则下不限版本的所有流程
(2) 可以选择性添加finished方法控制是否查询未完成的流程实例。在流程开启时,activiti同时在act_ru_execution表和act_hi_procinst表中创建了一条记录,在流程完成之前act_hi_procinst表中实例的结束时间为空

查看历史流程活动

查看某次流程执行过程中所经历的步骤
/*** 2.查看历史流程活动* 查看某次流程执行过程中所经历的步骤*/
@Test
public void queryHistoryActiviti() {// 获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();HistoryService historyService = processEngine.getHistoryService();// 查看历史流程实例活动对象HistoricActivityInstanceQuery activityInstanceQuery = historyService.createHistoricActivityInstanceQuery();// 添加过滤条件String processInstanceId = "101"; // 指定流程实例IDactivityInstanceQuery.processInstanceId(processInstanceId);// 执行查询List<HistoricActivityInstance> list = activityInstanceQuery.list();for (HistoricActivityInstance ha : list) {System.out.print("流程定义ID: "+ha.getProcessDefinitionId()+", ");System.out.print("开始活动ID: "+ha.getActivityId()+", ");System.out.print("开始活动名称: "+ha.getActivityName()+", ");System.out.print("开始时间: "+ha.getStartTime()+", ");System.out.println("结束时间: "+ha.getEndTime()+", ");}}


说明:通常查询历史流程活动都需要指定一个过滤条件,指定processInstanceId查看具体某一次流程执行过程中所经历的步骤

查看历史任务

/*** 3.查看历史任务数据* 任务与活动的区别:活动是一个动作,从A->B节点 是一个活动* 任务是执行某个节点*/
@Test
public void queryHistorTask() {// 获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();HistoryService historyService = processEngine.getHistoryService();// 查看历史流程实例任务HistoricTaskInstanceQuery taskInstanceQuery = historyService.createHistoricTaskInstanceQuery();// 添加过滤条件String processInstanceId = "101"; // 指定流程实例IDtaskInstanceQuery.processInstanceId(processInstanceId);// 执行查询List<HistoricTaskInstance> list = taskInstanceQuery.list();for (HistoricTaskInstance taskInstance : list) {System.out.print("任务名称:"+taskInstance.getName()+", ");System.out.print("执行者:"+taskInstance.getAssignee()+", ");System.out.println("任务开始时间:"+taskInstance.getStartTime());}}

流程变量

流程变量在整个工作流中扮演很重要的作用。例如:请假流程中有请假天数、请假原因等一些参数都为流程变量的范围。流程变量的作用域范围是流程实例。也就是说各个流程实例的流程变量是不相互影响的。

添加流程变量

在启动流程实例时

在启动流程实例时,可以添加流程变量。这是添加流程变量的一种时机。
/*** 1. 在流程启动时添加流程变量*/
@Test
public void startFlow() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RuntimeService runtimeService = processEngine.getRuntimeService();String processDefinitionKey = "QJLC";Map<String, Object> variables = new HashMap<>();variables.put("请假天数", "3");variables.put("请假原因", "家里有事");ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey, variables);System.out.println("pid: "+processInstance.getId());
}


说明:

(1) 在启动流程实例时,通过重载startProcessInstanceByKey方法 可以加载流程变量
(2) 此方法的第二个参数要求是Map<String,Object>类型,意味着可以添加多个流程变量
(3) 当这段代码执行完毕后,会在数据库表ACT_RU_VARIABLE中添加两行记录

在办理任务或任务提交时

在办理任务时或者有时候任务办理完成以后,需要传递一些信息到系统中,这时可以使用TaskService类来添加流程变量。
/*** 1.2 在办理任务和任务提交时添加流程变量*/
@Test
public void completeTask_setVar() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();String taskId = "606";Map<String, Object> variables1 = new HashMap<>() ;variables1.put("请假日期", "明天");/*** 在任务办理的过程中提交流程变量*/processEngine.getTaskService().setVariables(taskId, variables1);// 一次只能设置一个变量processEngine.getTaskService().setVariable(taskId, "请假送礼", "两盒红塔山");/*** 在任务提交时,设置流程变量*/Map<String, Object> variables2 = new HashMap<>();variables2.put("批注", "请领导批准");processEngine.getTaskService().complete(taskId, variables2);
}
查看ACT_RU_VARIABLE表
说明:
(1) 利用setVariables方法在任务办理的过程中可以添加多个流程变量
(2) 利用setVariable方法在任务办理过程中可以添加一个流程变量
(3) TaskService有一个重载complete的方法 可以在提交任务时提交流程变量

执行任务实例时

因为流程变量的作用域就是流程实例,所以可以为流程实例设置流程变量,为流程实例设置了流程变量,在该流程的任何阶段都可以获取得到。
/*** 1.3 为指定的流程实例设置流程变量*/
@Test
public void execution_setVar() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RuntimeService runtimeService = processEngine.getRuntimeService();String executionId = "601";Map<String, Object> variables = new HashMap<>();variables.put("value", "流程变量");// 在流程实例的办理过程中设置流程变量runtimeService.setVariables(executionId, variables);// runtimeService.setVariable(executionId, variableName, value);// 在流程实例提交时设置流程变量// runtimeService.signal(executionId, variables);
}
查看ACT_RU_VARIABLE表

获取流程变量

通过runtimeService获取

/*** 2.1 获取流程变量(使用RuntimeService)*/
@Test
public void runtimeServ_getVar() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RuntimeService runtimeService = processEngine.getRuntimeService();System.out.println("=======指定单个属性获取变量=========");String executionId = "601";String variableName = "请假原因";String value = (String) runtimeService.getVariableLocal(executionId, variableName);System.out.println(variableName+"="+value);System.out.println("=======指定多个属性获取变量=========");Collection<String> variableNames = new ArrayList<>();variableNames.add("请假天数");variableNames.add("请假日期");Map<String, Object> variables = runtimeService.getVariables(executionId, variableNames );for (String key : variables.keySet()) {System.out.println(key+"="+variables.get(key));}System.out.println("=======指定全部属性获取变量=========");Map<String, Object> variables2 = runtimeService.getVariables(executionId);for (String key : variables2.keySet()) {System.out.println(key+"="+variables2.get(key));}
}

通过TaskService获取

/*** 2.2 通过TaskService 获取*/
@Test
public void taskSer_getVar() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();String taskId = "705";Map<String, Object> variables = processEngine.getTaskService().getVariables(taskId );for (String key : variables.keySet()) {System.out.println(key+"="+variables.get(key));}
}

流程变量范围

官方给出的流程变量类型如下:

从图中可以看出包括了大部分封装类型和Date、String都实现了Serializable接口的类的类型。

JavaBean类型流程变量

(1) 加入一个JavaBean,此JavaBean必须实现Serializable接口
package activiti.lx.variable;import java.io.Serializable;public class User implements Serializable {private Long id;private String name;public User() {}public User(Long id, String name) {this.id = id;this.name = name;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}}

(2) 设置JavaBean类型流程变量

/*** 3.1 设置JavaBean流程变量*/
@Test
public void setJavaBeanVar() {String executionId = "601";Map<String, Object> variables = new HashMap<>();variables.put("user", new User(1L, "小明"));ProcessEngines.getDefaultProcessEngine().getRuntimeService().setVariables(executionId, variables);
}

(3) 获取JavaBean流程变量

/*** 3.2 获取JavaBean流程变量*/
@Test
public void getJavaBeanVar() {String executionId = "601";Map<String, Object> variables = ProcessEngines.getDefaultProcessEngine().getRuntimeService().getVariables(executionId);for (Entry<String, Object> entry : variables.entrySet()) {if (entry.getKey().equals("user")) {User user = (User) entry.getValue();System.out.println(user.getId());System.out.println(user.getName());}}
}

流程定义语言

流程(process)

process是bpmn文件中一个流程的根元素,一个流程代表一个工作流

顺序流(sequenceFlow)

顺序流是连接两个流程节点的连线,代表一个节点的出口。流程执行完一个节点后,会沿着节点的所有外出顺序流继续执行。也就是说,BPMN2.0默认的行为就是并发的:两个外出的顺序流会创造两个单独的并发流程分支。
顺序流(sequenceFolw)是process的子节点
顺序流主要由4个属性组成:
id:唯一标识,用来区分不同的顺序流。
sourceRef:连接线的源头节点id。
targetRef:连接线的目标节点id。
name(可选):连线的名称,不涉及业务,主要用于显示。
说明:
(1) 结束节点没有出口
(2) 其他节点有一个或多个出口。如果有一个出口,则代表是一个单线流程;如果有多个出口,则代表的是开启并发流程。

节点

主要有开始节点、结束节点、任务节点、网关节点、事件节点

开始事件节点(startEvent)

开始事件节点用来指明流程在哪里开始。开始事件的类型(流程在接收事件时启动还是在指定的时间启动等等),这通过事件中不同的小图表来展示。在XML中,这些类型是通过声明不通话的子元素来区分的。

空开始事件

空开始事件在技术上意味着没有指定启动流程实例的触发条件。是最常用的一种开始节点,意味着流程启动需要手动触发,通过API的startProcessInstanceByXXX方法。
ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();

图形标记:空开始事件显示成一个圆圈,没有内部图表(没有触发类型)


XML结构如下:
    <startEvent id="startevent1" name="Start"></startEvent>

定时开始事件

定时开始事件用来在指定的时间创建流程实例。它可以用于只启动一次的流程和应该在特定的时间间隔启动多次的流程。

注意:
1.子流程不能使用定时开始事件
2. 定时开始事件在流程发布后就会开始计算时间,不需要再手动调用startProcessInstanceByXXX 否则会导致启动过多的流程
3. 当包含定时开始事件的新版本流程部署时,对应的上一个定时器就会被删除。这是因为通常不希望自动启动旧版本流程的流程实例。
图形标记
定时开始事件显示为一个圆圈,内部是一个表
配置界面:
Time duration:每隔多长时间执行一次(无限循环)
Time date:在指定的时间执行一次
Time Cycle:按照一定格式配置,从某时间点开始计时,一共执行次数,执行的时间间隔
注意:只能选取使用一种配置
XML内容:
定时开始事件的XML内容是普通开始事件的声明,包含一个关于定时定义的子元素。
示例1:
表示:流程一共会执行4次,每次时间间隔为5分钟,从2013年9月18日,12:13分开始计时
<startEvent id="timerstartevent1" name="Timer start"><timerEventDefinition><timeCycle>R4/2013-09-18T12:13/PT5M</timeCycle></timerEventDefinition>
</startEvent>

示例2:


表示流程根据配置的时间启动一次
<startEvent id="timerstartevent1" name="Timer start"><timerEventDefinition><timeDate>2013-10-31T23:59:24</timeDate></timerEventDefinition>
</startEvent>

结束事件节点

结束事件表示(子)流程(分支)的结束。结束事件都是触发事件。这是说当流程达到结束时间,会触发一个结果。结果的类型是通过事件的内部黑色图标表示的。

空结束事件

空结束事件意味着到达事件时不会指定抛出的结果。 这样,引擎会直接结束当前执行的分支,不会做其他事情。
图形标记:
空结束事件是一个粗边圆圈,内部没有小图表(无结果类型)
 
XML内容:
空结束事件的XML内容是普通结束事件定义,不包含子元素 (其他结束事件类型都会包含声明类型的子元素)。

   <endEvent id="endevent4" name="End"></endEvent>

任务节点(TASK)

接收任务节点(receiveTask)

接收任务是一个简单任务,它会等待对应消息的到达。当前,官方只实现了这个任务的语义.当流程达到接收任务,流程状态会保存到数据库中。
在任务创建后,意味着流程会进入等待状态,直到引擎接收了一个特定的消息,才能触发流程穿过接收任务继续执行。
图形标记:
接收任务显示为一个任务(圆角矩形),右上角有一个消息小标记。 消息是白色的(黑色图标表示发送语义)

XML内容
<receiveTask id="receivetask1" name="Receive Task"></receiveTask>
当前任务(一般指机器自动完成,但需要耗费一定时间的工作)完成后,向后推移流程,可以调用runtimeService.signal(executionId),传递接收任务上流程的id
演示如下:
(1) 绘制流程图
为了方便测试,删除数据库中所有的ACT开头的表
执行的关键代码如下:
(2)初始化环境,加载新的流程图(流程定义规则)
// 初始化测试环境
@Test
public void deploy() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/task/receive/receiveTask.bpmn").addClasspathResource("activiti/lx/task/receive/receiveTask.png").name("部署测试接收任务");Deployment deploy = deploymentBuilder.deploy();System.out.println("部署ID:" + deploy.getId() + "\t部署名称" + deploy.getName());
}


(3) 启动流程实例

// 启动流程实例
@Test
public void startProcessInst() {String processDefinitionKey = "receiveProcess";ProcessInstance processInstance = ProcessEngines.getDefaultProcessEngine().getRuntimeService().startProcessInstanceByKey(processDefinitionKey);System.out.println("流程实例ID:" + processInstance.getId());
}


查看任务表,张三有一个代办任务

(4) 查询第一个任务节点(张三的任务)并完成
// 办理任务(查询第二个节点张三的任务并进行办理)
@Test
public void completeSecond() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();Task task = processEngine.getTaskService().createTaskQuery().taskAssignee("张三").singleResult();System.out.println(task.getId()); if (task!=null) {processEngine.getTaskService().complete(task.getId());}
}

再次查看任务表:


发现第一个"发出申请"任务节点完成之后,任务表没有任务,这是因为Receive task 不是由具体的人来完成的是由机器完成某耗时操作后发出signal信号来完成的
(5) 处理Receive Task
// 处理Receivetask
@Test
public void signal() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();Execution execution = processEngine.getRuntimeService().createExecutionQuery() // 查询活动分支.processInstanceId("101")  //整个流程实例id.activityId("receivetask1")   //Receive task的活动ID.singleResult();assertNotNull(execution);processEngine.getRuntimeService().signal(execution.getId());
}

处理完ReceiveTask任务节点后 查看任务表:


说明:在处理完第二个任务节点后就将流程推移到第三个流程任务节点了,

邮件任务节点

邮件任务节点是用来发送邮件服务的,当任务推移到邮件节点时,不用手动触发由Activiti根据邮件的相关配置进行邮件的发送
图形标记:左上角有一个邮件图标,实心黑色的。
(1) 要想发送邮件,需要做如下配置
在activiti-context.xml或activiti.cfg.xml中添加如下配置:
<!-- 配置流程引擎配置对象 -->
<bean id="processEngineConfiguration"class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration"><property name="jdbcDriver" value="com.mysql.jdbc.Driver" /><property name="jdbcUrl" value="jdbc:mysql:///activiti" /><property name="jdbcUsername" value="root" /><property name="jdbcPassword" value="root" /><property name="databaseSchemaUpdate" value="true" /><!-- Activiti默认提供了4种历史流程none:不保存任何历史记录,可以提高系统性能activity:保存所有的流程实例、任务、活动信息audit:也是activiti的默认级别,保存所有的流程实例、任务、活动、表单属性full:最完整的记录,除了包含audit级别的信息之外还能保存流程变量等信息--><property name="history" value="activity"/><!-- 邮件服务器配置 --><property name="mailServerHost" value="smtp.126.com"/> <!-- 邮件服务器地址 --><property name="mailServerPort" value="25"/> <!-- 邮件服务器端口 --><property name="mailServerUsername" value="liuxun1993728"/> <!-- 邮件服务器的用户名 --><property name="mailServerPassword" value="liuxun1993728"/>
</bean>

(2) 绘制流程图,在邮件任务节点上做配置

点击邮件任务节点,做如下配置:

生成的邮件任务节点的XML内容如下:
<serviceTask id="mailtask1" name="测试邮件任务" activiti:type="mail"><extensionElements><activiti:field name="to"><activiti:string><![CDATA[2652790899@qq.com]]></activiti:string></activiti:field><activiti:field name="from"><activiti:string><![CDATA[liuxun1993728@126.com]]></activiti:string></activiti:field><activiti:field name="subject"><activiti:string><![CDATA[Activiti发送邮件测试]]></activiti:string></activiti:field><activiti:field name="charset"><activiti:string><![CDATA[utf-8]]></activiti:string></activiti:field><activiti:field name="html"><activiti:string><![CDATA[<h1>测试邮件发送<h1>
<a href=‘https://8888av.co/‘>点击测试</a>]]></activiti:string></activiti:field></extensionElements>
</serviceTask>

(3) 部署流程规则,启动流程实例进行测试

// 初始化测试环境
@Test
public void deploy() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/task/email/emailTask.bpmn").addClasspathResource("activiti/lx/task/email/emailTask.png").name("部署测试邮件任务");Deployment deploy = deploymentBuilder.deploy();System.out.println("部署ID:" + deploy.getId() + "\t部署名称" + deploy.getName());
}// 启动流程实例
@Test
public void startProcessInst() {String processDefinitionKey = "emailProcess";ProcessInstance processInstance = ProcessEngines.getDefaultProcessEngine().getRuntimeService().startProcessInstanceByKey(processDefinitionKey);System.out.println("流程实例ID:" + processInstance.getId());
}



邮件成功发送。

用户任务节点(userTask)

用户任务用来设置必须由人员完成的工作。 当流程执行到用户任务,会创建一个新任务, 并把这个新任务加入到分配人或群组的任务列表中。
图形标记
用户任务显示成一个普通任务(圆角矩形),左上角有一个小用户图标。
 
XML内容
XML中的用户任务定义如下。id属性是必须的。 name属性是可选的。
<userTask id="theTask" name="Important task"/>                                  
用户任务也可以设置描述(实际上所有BPMN 2.0元素都可以设置描述)。 添加documentation元素可以定义描述。

<userTask id="theTask" name="Schedule meeting">
<documentation>Schedule an engineering meeting for next week with the new hire.
</documentation>

在实际应用中,用户接到任务后可以参照任务描述来办理任务,描述文本可以通过标准的java方法来获得:
task.getDescription()
任务分配
用户任务的办理都需要人工的参与。用户任务可以分为两大类。私有任务和公有任务(可接任务)。
私有任务
私有任务即有直接分配给指定用户的任务。只有一个用户可以成为任务的执行者。在activiti中,用户叫做执行者。拥有执行者的用户任务(即私有任务)对其他用户是不可见的。只能出现执行者的个人任务列表中.
直接把用户任务分配给指定用户使用assignee属性,XML代码如下:

<userTask id="theTask" name="my task" activiti:assignee="sirius"/>

Assignee属性对应的值为一个用户的ID。
直接分配给用户的任务可以通过TaskService像下面这样办理:

List<Task>tasks =taskService.createTaskQuery().taskAssignee("sirius").list();
Task task = tasks.get(0);// 假设任务集合的第一条是要办理的任务
taskService.complete(task.getId());

公有任务
有的用户任务在指派时无法确定具体的办理者,这时任务也可以加入到人员的候选任务列表中,然后让这些人员选择性认领和办理任务。
公有任务的分配可以分为指定候选用户和候选组两种。
a)把任务添加到一批用户的候选任务列表中,使用candidateUsers属性,XML内容如下:

<userTaskid="theTask"name="my task"activiti:candidateUsers="sirius,kermit"/>

candidateUsers属性内为用户的ID,多个用户ID之间使用(半角)逗号间隔。
b) 把任务添加到一个或多个候选组下,这时任务对组下的所有用户可见,首先得保证每个组下面有用户,通过IdentityService对象创建用户和组,然后把用户添加到对应的组下。
然后配置组任务,使用candidateGroups属性,XML内容如下:

<userTask  id="theTask"   name="my task"    activiti:candidateGroups="testGroup,developGroup"/>

间接分配给用户的任务,可以通过TaskService像下面这样操作:

List<Task>tasks =taskService.createTaskQuery().taskCandidateUser("sirius").list();
Task task = tasks.get(0);// 假设任务集合的第一条是要办理的任务
String taskId = task.getId();
taskService.claim(taskId ,“sirius”); //认领任务,让用户成为任务的执行者
taskService.complete(taskId );

说明:
1.要维护用户和组得使用用户管理服务对象,使用processEngine得到IdentityService。
2.要分配组任务必须先创建组,而且组下得有用户,用户和组的最关键属性是ID。
3.使用newUser(userId)和newGroup(groupId)创建用户 和组。
4.使用createMembership(userId,groupId)把用户挂到 组下。
5.办理候选任务,首先得认领任务,让用户成为任务的执行者。
如果上面的方式还不够灵活,那么我们也可以自定义一个任务分配处理器,通过代码的方式来动态设置任务的属性。XML代码如下:

<userTask id="task1" name="My task"> <extensionElements>
<activiti:taskListener event="create" class="org.activiti.MyAssignmentHandler"/>  </extensionElements>
</userTask>

DelegateTask会传递给TaskListener的实现, 通过它可以设置执行人,候选人和候选组:

Public class MyAssignmentHandler implements TaskListener {Public void notify(DelegateTask delegateTask){ // 执行用户搜索相关代码    ...// 然后把获取到的用户通过下面方法,设置给当前触发事件的任务delegateTask.setAssignee("sirius");//delegateTask.addCandidateUser("kermit");//delegateTask.addCandidateGroup("testGroup"); ...
}
}

receive task、email task、user Task虽然都可以统称为任务节点,但是还是有本质区别:
1.receiveTask主要代表机器自动执行的,userTask代表人工干预的。
2.receiveTask任务产生后会在act_ru_execution表中新增一条记录,而userTask产生后会在act_ru_execution和act_ru_task(主要记录任务的发布时间,办理人等信息)中各产生一条记录。
3.receiveTask任务提交方式使用RuntimeService的signal方法提交,userTask任务提交方式使用TaskService的complete方法提交。

4. email task 不会操作表,但是需要进行邮件服务和配置 自动触发 
用户任务实例测试
(1) 绘制流程图
(2) 对流程进行配置
选中"提交申请节点" 进行配置 私人任务
选中“审核1” 为任务指定多个候选人(公共任务)
选中“审核2” 配置多个候选组
(3)使用任务监听器为审核3任务节点 配置启动任务时指定任务执行人
3.1 创建一个任务监听器类实现TaskListener接口
package activiti.lx.task.usertask;import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;public class MyTaskListener implements TaskListener {@Overridepublic void notify(DelegateTask delegateTask) {// 根据业务从数据库中查询 相关人员// .....业务查询代码// 然后把获取到的用户通过下面方法,设置给当前触发事件的任务delegateTask.setAssignee("马云");}}

3.2选中"审核3" 配置绑定自定义的任务监听器(用于动态指定执行者)


(3) 执行代码如下:
package activiti.lx.task.usertask;import static org.junit.Assert.assertNotNull;import org.activiti.engine.IdentityService;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.identity.Group;
import org.activiti.engine.identity.User;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.Test;public class TestUserTask {// 初始化测试环境@Testpublic void deploy() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/task/usertask/userTask.bpmn").addClasspathResource("activiti/lx/task/usertask/userTask.png").name("测试用户任务");Deployment deploy = deploymentBuilder.deploy();System.out.println("部署ID:" + deploy.getId() + "\t部署名称" + deploy.getName());}// 启动流程实例@Testpublic void startProcessInst() {String processDefinitionKey = "userTaskProcess";ProcessInstance processInstance = ProcessEngines.getDefaultProcessEngine().getRuntimeService().startProcessInstanceByKey(processDefinitionKey);System.out.println("流程实例ID:" + processInstance.getId());}// 办理任务(查询任务节点"提交申请"张三的任务并进行办理)@Testpublic void completeSecond() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();Task task = processEngine.getTaskService().createTaskQuery().taskAssignee("张三").singleResult();System.out.println(task.getId());if (task != null) {processEngine.getTaskService().complete(task.getId());}}// 候选人拾取公共任务 并进行办理("审核1" 任务节点)@Testpublic void claimTask() {String processDefinitionKey = "userTaskProcess";Task task = ProcessEngines.getDefaultProcessEngine().getTaskService().createTaskQuery().processDefinitionKey(processDefinitionKey).taskCandidateUser("王五").singleResult();assertNotNull(task);ProcessEngines.getDefaultProcessEngine().getTaskService().claim(task.getId(), "王五"); // 拾取任务ProcessEngines.getDefaultProcessEngine().getTaskService().complete(task.getId()); // 办理任务}// 候选组获取任务并进行办理("审核2")@Testpublic void candidateUserResolveTask() {String processDefinitionKey = "userTaskProcess";Task task = ProcessEngines.getDefaultProcessEngine().getTaskService().createTaskQuery().processDefinitionKey(processDefinitionKey).taskCandidateGroup("group1").singleResult();assertNotNull(task);IdentityService identityService = ProcessEngines.getDefaultProcessEngine().getIdentityService();User user = identityService.newUser("groupUser1");user.setFirstName("赵");user.setLastName("六");Group group = identityService.newGroup("group1");group.setName("群组测试");identityService.saveUser(user);identityService.saveGroup(group);identityService.createMembership(user.getId(),group.getId());ProcessEngines.getDefaultProcessEngine().getTaskService().claim(task.getId(), user.getId());ProcessEngines.getDefaultProcessEngine().getTaskService().complete(task.getId());}// 办理任务(查询任务节点"审核3"马云的任务并进行办理)@Testpublic void completeEnd() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();Task task = processEngine.getTaskService().createTaskQuery().taskAssignee("马云").singleResult();System.out.println(task.getId());if (task != null) {processEngine.getTaskService().complete(task.getId());}}}

网关(gateWay)

网关用来控制流程的流向。
网关显示成菱形图形,内部有一个小图标。图标表示网关的类型。

排他网关

排他网关(也叫异或 XOR) 网关,或更技术性的叫法是"基于数据的排他网关",用来在流程中实现决策
图形标记
排他网关显示成一个普通网关(比如,菱形图形),内部是一个“X”图标,表示异或(XOR)语义。 注意,没有内部图标的网关,默认为排他网关。 BPMN 2.0规范不允许在同一个流程定义中同时使用没有X和有X的菱形图形。
XML内容
排他网关的XML内容是很直接的:用一行定义了网关, 条件表达式定义在外出顺序流中。 参考条件顺序流 获得这些表达式的可用配置。
 
它对应的XML内容如下:

<exclusiveGatewayid="exclusiveGw"name="Exclusive Gateway"/>
<sequenceFlowid="flow2"sourceRef="exclusiveGw"targetRef="theTask1">  <conditionExpressionxsi:type="tFormalExpression">${input == 1}</conditionExpression>
</sequenceFlow>
<sequenceFlowid="flow3"sourceRef="exclusiveGw"targetRef="theTask2">    <conditionExpressionxsi:type="tFormalExpression">${input == 2}</conditionExpression>
</sequenceFlow>
<sequenceFlowid="flow4"sourceRef="exclusiveGw"targetRef="theTask3">  <conditionExpressionxsi:type="tFormalExpression">${input == 3}</conditionExpression>
</sequenceFlow>

说明:
1.一个排他网关对应一个以上的顺序流
2.由排他网关流出的顺序流都有个conditionExpression元素,在内部维护返回boolean类型的决策结果。
3.决策网关只会返回一条结果。当流程执行到排他网关时,流程引擎会自动检索网关出口,从上到下检索如果发现第一条决策结果为true或者没有设置条件的(默认为成立),则流出。
4.如果没有任何一个出口符合条件则抛出异常。

实例演示如下:
(1) 绘制流程
(2) 设置条件方式 分别点击从排他网关流出的两条顺序流,设置条件
生成的XML核心内容如下:
 <process id="gateWay1" name="排他网关" isExecutable="true"><startEvent id="startevent1" name="Start"></startEvent><userTask id="usertask1" name="提交报销申请" activiti:assignee="张三"></userTask><sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow><exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway><userTask id="usertask2" name="财务人员审批" activiti:assignee="李四"></userTask><userTask id="usertask3" name="财务主管审批" activiti:assignee="王五"></userTask><sequenceFlow id="flow2" sourceRef="usertask1" targetRef="exclusivegateway1"></sequenceFlow><sequenceFlow id="flow3" sourceRef="exclusivegateway1" targetRef="usertask2"><conditionExpression xsi:type="tFormalExpression"><![CDATA[${money <= 500}]]></conditionExpression></sequenceFlow><sequenceFlow id="flow4" sourceRef="exclusivegateway1" targetRef="usertask3"><conditionExpression xsi:type="tFormalExpression"><![CDATA[${money > 500}]]></conditionExpression></sequenceFlow><endEvent id="endevent1" name="End"></endEvent><sequenceFlow id="flow5" sourceRef="usertask2" targetRef="endevent1"></sequenceFlow><sequenceFlow id="flow6" sourceRef="usertask3" targetRef="endevent1"></sequenceFlow></process>

(3)核心代码

package activiti.lx.gateway.exclusive;import java.util.HashMap;
import java.util.Map;import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.Test;
/*** 测试排他网关* @author liuxun**/
public class TestExclusiveGateWay {// 初始化测试环境@Testpublic void deploy() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/gateway/exclusive/gateWay1.bpmn").addClasspathResource("activiti/lx/gateway/exclusive/gateWay1.png").name("部署测试排他网关");Deployment deploy = deploymentBuilder.deploy();System.out.println("部署ID:" + deploy.getId() + "\t部署名称" + deploy.getName());}// 启动流程实例@Testpublic void startProcessInst() {String processDefinitionKey = "gateWay1";ProcessInstance processInstance = ProcessEngines.getDefaultProcessEngine().getRuntimeService().startProcessInstanceByKey(processDefinitionKey);System.out.println("流程实例ID:" + processInstance.getId());}// 办理任务(查询第二个节点张三的任务并进行办理)@Testpublic void completeSecond() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();Task task = processEngine.getTaskService().createTaskQuery().taskAssignee("张三").singleResult();System.out.println(task.getId());if (task != null) {Map<String, Object> variables = new HashMap<String, Object>();variables.put("money", 800);processEngine.getTaskService().complete(task.getId(), variables );}}
}

测试结果:
张三提交报销申请后,因为金额=800>500 需要交给财务主管审批 数据库中Task表内会有财务主管王五的任务

如果报销金额money<=500 任务会交给财务人员李四审批

并行网关(parallelGateWay)

网关也可以表示流程中的并行情况。最简单的并行网关是parallelGateWay,它允许将流程 分成多条分支,也可以把多条分支 汇聚到一起。
图形标记
并行网关显示成一个普通网关(菱形)内部是一个“加号”图标, 表示“与(AND)”语义。
 
XML内容
定义并行网关只需要一行XML:
<parallelGateway id="myParallelGateway"/>
实际发生的行为(分支,聚合,同时分支聚合), 要根据并行网关的顺序流来决定。
上面例子中,流程启动之后,会创建两个任务:

ProcessInstancepi =runtimeService.startProcessInstanceByKey("forkJoin");
TaskQueryquery=taskService.createTaskQuery().processInstanceId(pi.getId()).orderByTaskName().asc();
List<Task>tasks =query.list();
assertEquals(2,tasks.size());
Task task1 =tasks.get(0);
assertEquals("Receive Payment",task1.getName());
Tasktask2 =tasks.get(1);
assertEquals("Ship Order",task2.getName());

当两个任务都完成时,第二个并行网关会汇聚两个分支,因为它只有一条外出连线, 不会创建并行分支, 只会创建归档订单任务。
说明:
1.并行网关的功能是基于进入和外出的顺序流的:
分支(fork): 并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
汇聚(join): 所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
2.并行网关的进入和外出都是使用相同节点标示
3.如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
4.并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
5.并行网关不需要是“平衡的”(比如, 对应并行网关的进入和外出节点数目相等)。如图中标示是合法的: 

实例测试如下:
(1) 创建流程
(2)部署流程规则,启动流程实例
// 初始化测试环境
@Test
public void deploy() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/gateway/parallel/gateWay2.bpmn").addClasspathResource("activiti/lx/gateway/parallel/gateWay2.bpmn").name("部署测试并行网关");Deployment deploy = deploymentBuilder.deploy();System.out.println("部署ID:" + deploy.getId() + "\t部署名称" + deploy.getName());
}// 启动流程实例
@Test
public void startProcessInst() {String processDefinitionKey = "gateWay2";ProcessInstance processInstance = ProcessEngines.getDefaultProcessEngine().getRuntimeService().startProcessInstanceByKey(processDefinitionKey);System.out.println("流程实例ID:" + processInstance.getId());
}
启动任务实例后,查看任务表和流程实例表
ACT_RU_EXECUTION
ACT_RU_TASK
可以发现 启动了两个任务,三个流程(主流程即流程实例,两个并行的子流程)
(3) 处理买家和卖家的任务 查看表中数据
private void completeTaskByDefKeyAndAssignee(String processDefinitionKey,String assignee) {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();List<Task> tasks = processEngine.getTaskService().createTaskQuery().taskAssignee(assignee).processDefinitionKey(processDefinitionKey).list();if (tasks.size()>0) {for (Task task : tasks) {processEngine.getTaskService().complete(task.getId());}}
}
// 办理任务
@Test
public void completeSecond() {String processDefinitionKey = "gateWay2";//String assignee = "买家";String assignee = "卖家";completeTaskByDefKeyAndAssignee(processDefinitionKey, assignee );
}
首先办理买家的任务(买家付款)
ACT_RU_EXECUTION
ACT_RU_TASK
再办理两次卖家的任务
ACT_RU_EXECUTION
ACT_RU_TASK

最后办理买家的任务(买家收货)

ACT_RU_EXECUTION
ACT_RU_TASK


另个并发分支子流程执行完毕,流程汇聚执行完毕。

监听器(Listener)

在流程中我们有时会对整个流程或者一个节点的某种状态做出相应的处理。这时就会用到监听器。
在Activiti中流程的监听主要分为两大类,执行监听器和任务监听器。

执行监听器(ExecutionListener)

执行监听器可以执行外部Java代码或执行表达式,当流程定义中发生了某个事件时。可以捕获的事件有:
(1) 流程实例的启动和结束
(2) 选中一条连线
(3) 节点的开始和结束
(4) 网关的开始和结束
(5) 中间事件的开始和结束
(6) 开始事件结束和结束事件开始
现在有这样一个流程,只包含开始、结束、接收任务和用户任务4个节点 
配置执行监听器的XML代码如下:
<extensionElements><activiti:executionListener event="start" class="activiti.lx.listener.executionlistener.MyExecutionListener"></activiti:executionListener><activiti:executionListener event="end" class="activiti.lx.listener.executionlistener.MyExecutionListener"></activiti:executionListener>
</extensionElements>

说明:

1.任务监听器支持以下属性
Event(必选): 任务监听器会被调用的任务类型,可能的类型为:
  • start:流程节点创建后触发。
  • end:当任务完成,并未从运行时的数据表中删除时触发。
  • take:当任务完成后,流程流出时触发(只适用于连线(顺序流))。
Class:必须调用的代理类,这个类必须实现org.activiti.engine.delegate.ExecutionListener接口。
自定义执行监听器的实现类代码如下:
package activiti.lx.listener;import java.util.ArrayList;
import java.util.List;import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;/*** 自定义执行监听器* * @author liuxun**/
public class MyExecutionListener implements ExecutionListener {@Overridepublic void notify(DelegateExecution execution) throws Exception {// 获取日志流程变量List<String> logs = (List<String>) execution.getVariable("logs");if (logs == null) { //第一次则新建logs = new ArrayList<>();}// 把当前节点名称和监听事件名称添加至流程变量中logs.add(execution.getCurrentActivityName()+" "+execution.getEventName());// 将更新的日志变量放入流程execution.setVariable("logs", logs);}}

2. 执行监听器配置可以放在以下三个地方,如图:


a) 监听整个流程的启动和结束状态,配置为process节点的子元素,如图①
b) 监听一个节点的启动和结束状态,配置为一个节点的子元素,如图图②和图③
c) 监听一条连线的执行,配置在sequenceFlow节点的内部,只有take一种事件,如图④
启动流程测试代码如下:
@Test
public void startProcess() {// 部署ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();repositoryService.createDeployment().addClasspathResource("activiti/lx/listener/executionlistener/executionLogListener.bpmn").deploy();// 启动RuntimeService runtimeService = processEngine.getRuntimeService();ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("execution");assertNotNull(processInstance.getId());// 推移流程runtimeService.signal(processInstance.getId());Task task = processEngine.getTaskService().createTaskQuery().taskAssignee("user").processDefinitionKey("execution").singleResult();processEngine.getTaskService().complete(task.getId());// 代码执行完毕,流程即进入历史,查看流程变量logsList<String> logs = (List<String>) processEngine.getHistoryService().createHistoricVariableInstanceQuery().processInstanceId(processInstance.getId()).variableName("logs").singleResult().getValue();System.out.println("流程结束,日志内容为:"+logs);
}

测试结果如下:

任务监听器(TaskListener)

任务监听器可以在发生对应的任务相关事件时执行自定义Java逻辑或表达式
任务监听器只能添加到流程定义中的用户任务中。在之前的用户任务节点上添加任务。
1.任务监听器支持以下属性:
event(必选):任务监听器会被调用的任务类型。 可能的类型为:

  • create:任务创建并设置所有属性后触发。
  • assignment:任务分配给一些人时触发。 当流程到达userTask,assignment事件 会在create事件之前发生。 这样的顺序似乎不自然,但是原因很简单:当获得create时间时, 我们想获得任务的所有属性,包括执行人。
  • complete:当任务完成,并尚未从运行数据中删除时触发。

class:必须调用的代理类。这个类必须实现org.activiti.engine.delegate.TaskListener接口。Java代码如下:

package activiti.lx.listener;import java.util.ArrayList;
import java.util.List;import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;public class MyTasksListener implements TaskListener {@Overridepublic void notify(DelegateTask delegateTask) {// 获取日志流程变量List<String> logs = (List<String>) delegateTask.getVariable("logs");if (logs == null) { // 第一次创建logs = new ArrayList<>();}// 把当前节点名称和监听事件名称添加到流程变量中logs.add(delegateTask.getName()+" "+delegateTask.getEventName());// 将更新的日志变量放入流程delegateTask.setVariable("logs", logs);}}

2.运行测试结果得到如下代码:

流程结束,日志内容为:[Start start, Receive Task start, Receive Task end, Receive Task take, User Task start,User Task assignment, User Task create, User Task complete, User Task end, End end]

表达式的使用

UEL是java EE6规范的一部分,UEL(Unified Expression Language)即统一表达式语言,activiti支持两个UEL表达式:UEL-value和UEL-method
实例如下:
创建JavaBean 变量类型
public class User implements Serializable {private static final long serialVersionUID = 7717000074223077256L;private Long id;private String name;public User() {}// 设置对应的setter和getter方法// ......
}

创建为UEL-Method提供变量的Bean

package activiti.lx.expression;public class AcquireUser {public String getUserNameById(String id) {// 根据业务从数据库中查询// ......return "用户"+id;}
}

在配置文件中配置为UEL-Method提供变量的Bean(AcquireUser)


流程的配置如下:
1)配置UEL-Value 表达式 (简单类型)---------------------------------------------------------------

2)配置UEL-Value 表达式 (Bean类型)---------------------------------------------------------------

3)配置UEL-Method 表达式 ---------------------------------------------------------------
核心代码:
@Test
public void expressionStart() {ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 部署规则processEngine.getRepositoryService().createDeployment().addClasspathResource("activiti/lx/expression/expression.bpmn").name("测试uel表达式").deploy();// 启动实例String processDefinitionKey = "expression";String businessKey = "111"; // 业务逻辑单元标识,与具体业务一一对应的Map<String, Object> variables = new HashMap<String, Object>();variables.put("assignee", "张三"); // 设置变量variables.put("user", new User(1L, "李四"));variables.put("id", "1000");// 必须在任务启动时设置流程变量,才能解析对应的uel表达式ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey(processDefinitionKey, businessKey, variables);Task task1 = processEngine.getTaskService().createTaskQuery().processInstanceBusinessKey(businessKey).processInstanceId(processInstance.getId()).singleResult();assertNotNull(task1.getId());processEngine.getTaskService().complete(task1.getId()); // 办理第一个任务节点的任务Task task2 = processEngine.getTaskService().createTaskQuery().processInstanceBusinessKey(businessKey).processInstanceId(processInstance.getId()).singleResult();assertNotNull(task2.getId());processEngine.getTaskService().complete(task2.getId()); // 办理第二个任务节点的任务Task task3 = processEngine.getTaskService().createTaskQuery().processInstanceBusinessKey(businessKey).processInstanceId(processInstance.getId()).singleResult();assertNotNull(task3.getId());processEngine.getTaskService().complete(task3.getId()); // 办理第三个任务节点的任务// 任务查询完毕,查询历史数据List<HistoricTaskInstance> list = processEngine.getHistoryService().createHistoricTaskInstanceQuery().processInstanceBusinessKey(businessKey).processInstanceId(processInstance.getId()).list();for (HistoricTaskInstance hti : list) {System.out.print("任务ID:"+hti.getId() + ",");System.out.print("任务名称:"+hti.getName() + ",");System.out.println("代理人:"+hti.getAssignee() + ",");}
}

测试结果:

注意事项:凡是与UEL有关的流程变量都要在启动流程实例时进行设置进去 成为全局变量,否则会抛出异常。

Activiti与Spring集成

虽然可以自己手动来创建相应的API实例,但是在一个项目中这些API都应该以单例形式存在的。和Spring的集成主要就是把Activiti的主要对象交给Spring容器管理。
ProcessEngineFactoryBean
可以把流程引擎(ProcessEngine)作为一个普通的Spring bean进行配置。 类 org.activiti.spring.ProcessEngineFactoryBean是集成的切入点。这个bean需要一个流程引擎配置对象来创建流程引擎。集成Spring后,配置文件如下:
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"><!-- 加载jdbc属性文件 --><context:property-placeholder location="classpath:activiti/lx/spring/jdbc.properties"/><!-- 配置数据源 --><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="${driverClassName}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></bean><!-- 事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 配置一个spring提供的对象,用于创建一个流程引擎配置对象 --><bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"><property name="transactionManager" ref="transactionManager"/><property name="dataSource" ref="dataSource"/><property name="databaseSchemaUpdate" value="true"/></bean><!-- 创建流程引擎对象 --><bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean"><property name="processEngineConfiguration" ref="processEngineConfiguration"/></bean>
</beans>

jdbc.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///activiti
username=root
password=root

测试代码:

package activiti.lx.spring;import org.activiti.engine.ProcessEngine;
import org.activiti.engine.repository.DeploymentBuilder;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class SpringActivitiTest {@Testpublic void test1() {ApplicationContext ctx = new ClassPathXmlApplicationContext("activiti/lx/spring/applicationContext.xml");ProcessEngine processEngine = (ProcessEngine) ctx.getBean("processEngine");DeploymentBuilder deploymentBuilder = processEngine.getRepositoryService().createDeployment();deploymentBuilder.addClasspathResource("activiti/lx/processdefination/MyProcess.bpmn");deploymentBuilder.addClasspathResource("activiti/lx/processdefination/MyProcess.png");deploymentBuilder.deploy();}}
整体代码已上传至GitHub 地址:(https://github.com/LX1993728/Activiti_java_Demo)
大致结构如下:

JAVAWEB开发之工作流详解(二)——Activiti核心API的使用(流程定义和流程实例的管理、流程变量、监听器...)以及与Spring的集成相关推荐

  1. JAVAWEB开发之工作流详解(一)——Activiti的环境搭建、插件安装、核心API

    工作流的概念 工作流(Workflow),就是"业务过程的部分或整体在计算机应用环境下的自动化",它主要解决的是"使在多个参与者之间按照某种预定义的规则传递文档.信息或任 ...

  2. JAVAWEB开发之SpringMVC详解(二)——高级开发、数据回显、参数绑定集合、图片上传、json交互、validation校验、异常处理、RESTful支持、拦截器

    知识回顾 springmvc框架 用户请求url到DispatcherServlet前端控制器,相当于中央调度器,降低系统各组件之间的耦合度. DispatcherServlet前端控制器通过Hand ...

  3. Retrofit详解(二)(Retrofit核心流程)

    上一章已经介绍了Retrofit创建过程,这章介绍Retrofit Api Service创建与访问过程. Retrofit 相比Volley等网络框架一个最大区别就是它只需要声明接口,就可以访问网络 ...

  4. Android openGl开发详解(二)

    https://zhuanlan.zhihu.com/p/35192609 Android openGl开发详解(二)--通过SurfaceView,TextureView,GlSurfaceView ...

  5. 13.56M读卡器开发详解二(RC522驱动程序)

    13.56M读卡器开发详解二 1. 硬件接口介绍 根据上一篇的介绍,小编使用了RC522的SPI口通信方式和51单片机进行通信.硬件接口设计此处不再附图.只是将接口配置列写如下: sbit  spi_ ...

  6. JavaWeb中filter的详解及应用案例

    JavaWeb中filter的详解及应用案例 转载自:http://www.cnblogs.com/vanl/p/5742501.html 一:Filter介绍 Filter可认为是Servlet的一 ...

  7. java web编码详解_java web 开发 编码问题详解

    java web 开发 编码问题详解 浏览器 IE/FireFox ------------->Servlet容器-------------------------->显示页面 编码   ...

  8. [转]文件IO详解(二)---文件描述符(fd)和inode号的关系

    原文:https://www.cnblogs.com/frank-yxs/p/5925563.html 文件IO详解(二)---文件描述符(fd)和inode号的关系 ---------------- ...

  9. linux 进程间通信 dbus-glib【实例】详解二(下) 消息和消息总线(ListActivatableNames和服务器的自动启动)(附代码)

    linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...

最新文章

  1. HTMLButton控件下的Confirm()
  2. 20200605笔记
  3. js判断是否是ie浏览器且给出ie版本
  4. 【C++】35.判断一个文件是否存在、 查找字符串中的子串
  5. HashSet 和 LinkedHashSet 源码分析,竟如此简单!
  6. 查询成绩(要求用链表完成)
  7. python发邮件主机找不到_Python 使用QQ邮箱发邮件
  8. vue.js表格赋值_vue.js input框之间赋值方法
  9. linux telnet mysql_Linux下安装telnet(傻瓜教程)
  10. Msql快速学习基础知识------engines
  11. windwos安装Android NDK(Native Development Kit)
  12. MAC下配置 adb 环境变量
  13. 5、win7激活秘钥
  14. IDEA跟金山词霸的小bug
  15. '\xF0\x9F\x98\x82\xF0\x9F...'报错处理
  16. 八股总结(二)计算机网络与网络编程
  17. 图漾深度相机FS820-E1使用
  18. 聊聊GIS中的坐标系|再版
  19. 使用Excel2010条码控件碰到的问题及解决办法
  20. mysql命令添加用户名和密码_怎么给mysql添加用户名和密码

热门文章

  1. excel表格在线共享的方案
  2. java抽组件_GitHub - ysc/HtmlExtractor: HtmlExtractor是一个Java实现的基于模板的网页结构化信息精准抽取组件。...
  3. MSF复现Thinkphp漏洞
  4. 竞品分析:记账理财—随手记为何能独占鳌头
  5. DDD:我的购书清单(欢迎借读,邮费自理)
  6. 中石油集团不会整体上市
  7. linux软raid阅读笔记,linux自学笔记——RAID级别特性以及软RAID的实现
  8. Linux Shell脚本pause命令
  9. 一文读懂Spring动态配置多数据源---源码详细分析
  10. 战地2042 战地 6 建议 介入 流程