activiti学习笔记

在讲activiti之前我们必须先了解一下什么是工作流,什么是工作流引擎。

在我们的日常工作中,我们会碰到很多流程化的东西,什么是流程化呢,其实通俗来讲就是有一系列固定的步骤的事情。例如我们应聘职位,这会有一个流程,先笔试、一轮部门主管面、二轮经理面、三轮总经理面、四轮HR面。不管你是谁来面试,都是要经理这个步骤。

工作流其实也就是一个工作的流程,就像我们上面说的面试流程。当然我们生活不止这点点例子。只要你打开万孩具憎的钉钉,里面就有个审核模块,里面一大堆都是你在工作中会遇到的流程,什么请假流程、调薪流程、报销流程、调岗流程,一大堆。

那什么是工作流引擎呢?引擎是一个带动一个东西不断运动的工具,好像汽车引擎、飞机引擎、游戏引擎。很明显,工作流引擎就是带动工作流不断运动的工具,再简单点说那就是不断推动我们的工作的流程的一个工具。

明白了吧,工作流引擎就是一个工具,他的作用就仅仅是推动你的流程不断运转。

activiti就是一个工作流引擎,工作流引擎除了这个还有挺多的,但是其他我都没了解。

activiti能帮我们做什么?

前面我说过,工作流引擎就是推动你的流程不同运转的工具。我们的真实世界中的流程多种多样,成百上千,而且各不相同,甚至同一件事不同的人也有不同的流程。

那我们的流程各不相同,工作流引擎凭什么能帮我们推动流程运转呢?其实很简单,尽管我们各种流程都不尽相同,但其本质、其核心的一点肯定是不变的:流程发起----任务查询----任务完成----下一个任务----任务查询----任务完成----下一个任务-----结束。

不管什么流程,都是按照以上步骤不断进行的,变得只是我们的业务内容。

假如我们此时有个请假流程,员工填写请假申请单----》单子放部门老大桌面----》部门老大在桌面翻单子看----》部门老大签个名----》部门老大顺手把单子扔给经理-----》经理捡起单子看看-----》经理同意也签名了-----》经理让行政过来拿单子----》行政将请假申请入档----》请假成功。

套进我们的核心过程里面:

如果我们又有一个离职流程,员工填写离职通知书----》悄咪咪拿给人事----》人事看了申请觉得可以----》人事拿给部门老大瞧瞧-----》老大看看很惊讶-----》但老大还是签字同意----》你滚蛋了-----》结束。

再套进我们的核心过程:

上面两个例子,即便我们流程繁多,但步骤无非就那5个:发起、查询、转下一个任务、完成任务、结束。

而工作流说做的也就是控制这5个步骤。让我们只需要关心我们各个任务的自己的业务数据就行了,至于流程怎么走,任务怎么查询、任务怎么进入下一个环节,工作流帮我们管理。

注意啊,这并不是说我们就什么都不用管了,我们还是要调用工作流给我们的简单的api接口去完成这5个步骤。可能你会有点蒙,但是不要紧。下面我再给你们分析下我们不用工作流和用工作流具体怎么实现你就会更清晰了。

如果我们不用工作流,我们要怎么实现呢?

我们先拿请假申请里做例子。

员工填写请假申请单----》单子放部门老大桌面----》部门老大在桌面翻单子看----》部门老大签个名----》部门老大顺手把单子扔给经理-----》经理捡起单子看看-----》经理同意也签名了-----》经理让行政过来拿单子----》行政将请假申请入档----》请假成功

要完成请假申请,我们大概需要三张表

 第一步:员工填写请假申请单:

  这里我们要有一个页面,给人填写表单,然后在【请假申请记录表】里面增加一条记录

 第二步:部门老大查询:

  这里我们要有一个页面,给部门老大查询他的要审批的单子,查询那些 “当前申请环节” == “部门老大审核” 的记录,查出刚刚张三申请的单子

第三步:部门老大审批:

  这里我们要有一个页面给部门老大审批,部门老大同意了,提交了他的审核记录,然后在【审核记录表】里面增加一条记录

  同时也将请假申请记录的当前申请环节改成 “经理审核”

第四步:经理查询:

  这里要有一个页面,可以和第二步共用一个页面,但是查询逻辑不同,查询那些 “当前申请环节” == “经理审核” 的记录,查出张三申请的单子

第五步:经理审批:

  这里可以用第三步部门老大审批的页面。经理同意请假,提交了他的审核记录,然后在【审核记录表】里面增加一条记录

  同时也将请假申请记录的当前申请环节改成 “行政存档”

第六步:行政查询:

  这里要有一个页面,可以和第二步共用一个页面,但是查询逻辑不同,查询那些 “当前申请环节” == “行政存档” 的记录,查出张三申请的单子

第七步:行政存档:

  这里要有一个页面,给行政进行请假申请的存档,行政存档后,【请假入档记录表】增加一条记录

  同时也将请假申请记录的当前申请环节改成 “请假通过”

到目前为止你是不是觉得一切都很简单,就算不用工作流引擎也一样so easy。当初我在学activiti时也是这样子想的,明明自己写也很简单啊,为什么还要用工作流,没事找事么。

先别急,我们继续往下看,等加多一个流程的时候,我们就能感受到为什么要加工作流引擎了。

第二个离职流程:

员工填写离职通知书----》悄咪咪拿给人事----》人事看了申请觉得可以----》人事拿给部门老大瞧瞧-----》老大看看很惊讶-----》但老大还是签字同意----》你滚蛋了-----》结束。

可能会有人觉得我这个例子不太合理,离职哪里需要申请的,但往往世界就是这么不合理的,流程往往都会有奇葩的。

那么我们要完成这个离职流程,大概需要这4张表

 第一步:员工填写离职通知书:

  这里我们要有一个页面,给人填写表单,然后在【离职申请记录表】里面增加一条记录。注意,这里这个功能和之前的请假流程完全不一样,所以需要一个新的功能,无法复用。

 第二步:人事查询:

  这里我们要有一个页面,给人事查询离职申请,查询那些 “当前环节” == “人事审核” 的记录,查出张三申请的单子。注意,这里和之前的请假流程完全不一样,用的表都不同,所以需要一个新的功能,无法复用。

 第三步:人事审批:

  这里我们要有一个页面,给人事进行审批,填写审批意见。然后在【审核记录表】里面增加一条记录。注意,这里和之前的请假流程完全不同,用到的表都不一样,所以需要一个新的功能,无法复用。

  同时也将离职申请记录的当前环节改成 “部门领导交接”。

 第四步:部门领导查询:

  这里我们要有一个页面,给部门领导查询离职申请,查询那些 “当前环节” == “部门领导交接” 的记录,查出张三申请的单子。这里可以和第二步人事查询的页面复用

 第五步:部门领导交接:

  这里我们要有一个页面,给部门领导提交交接记录,【离职交接记录表】增加一条记录。这里无法和前面的页面复用。

  同时也将离职申请记录的当前环节改成 “待归档”。

 第六步:人事查询:

  这里我们要有一个页面,给人事查询离职申请,查询那些 “当前环节” == “待归档” 的记录,查出张三申请的单子。这里可以和第二步人事查询共用同一个页面。

第七步:人事查询:

  这里我们要有一个页面,给人事进行离职记录归档,没有页面可以复用,【离职记录表】增加一条记录。

  同时也将离职申请记录的当前环节改成 “已完成”。

到目前为止,我们两个流程,大致有9个页面:请假单填写、请假单查询、请假单审批、请假单归档、离职申请单填写、离职单查询、离职单审批、交接记录填写、离职归档。

而且每增加一个流程,我们都要把整个流程都实现一遍。甚至说我改变流程,增加一个节点,减少一个节点,任务节点按条件分支等等,不仅要实现每个任务节点的功能,还要自己实现节点流转控制的功能。

而我们用了activiti工作流后会怎样呢?

先弄请假申请。还是要三张表,但是要注意,此时请假申请记录表没有当前环节了,因为流程环节不归我们业务管了,有activiti管理了。

第一步:画好流程图,设置好各种参数

第二步:写一个页面,填写请假申请单,【请假申请记录表】增加一条记录,同时调用activiti启动一个流程实例

第三步:写一个页面,调用activiti的接口查询部门老大要完成的任务。activiti会自动管理任务的责任人,在查询任务时,只需传入当前用户的标识或所在用户组的标识,就可以查询到任务责任人为自己,或任务责任人组中有自己的一系列任务。这是因为activiti会从我们的流程图中读取到这个任务节点由谁处理。

这里需要注意,activiti可以帮我们找到当前用户的所有待处理任务,但是任务仍然需要我们自己和业务关联上,这样才具有真正的实际意义,回显在页面上才知道什么意思,不然就只是一个单纯的任务节点。

第四步:写一个页面,部门老大审核通过,【审核记录表】增加一条记录,调用activiti的接口完成这一节点。

第五步:经理查询自己的任务,这里无需重写页面,不管谁查任务都只需要传入自己的用户标识或用户组标识就可以。

第六步:因为内容和第四步一样,也可以直接复用页面和功能,【审核记录表】增加一条记录,调用activiti的接口完成这一节点。

这里说下为什么可以复用,因为部门老大和经理的什么操作都是相似的,不同的地方在于填写的审核意见,以及当前环节,审核意见是页面输入的,当前环节是去activiti获取的。

第七步:人事查询,这里和部门老大查询、经理查询一样,直接复用

第八步:人事归档,这里的处理和上面都不同,所以页面要新的

【请假入档记录表】增加一条记录

看上去是不是还是和以前一眼多步骤,但是仔细看下,就会发现,我们直接操作的表,其实只有我们的业务相关的表,请假申请记录的状态也不需要了,连查询任务我们要只是简单调用activiti的接口。整个流程运转我们都没有碰过。甚至乎查询任务我们都一直在复用

我们再加个离职申请流程看看

这里我们还是要四个表,不过离职申请记录表的当前环节不需要我们自己管了

 第一步:画一个离职流程图,这里流程图是很重要的,activiti就是靠他知道你都有哪些步骤,每个步骤有谁来执行

 第一步:员工填写离职通知书:

  这里我们要有一个页面,给人填写表单,然后在【离职申请记录表】里面增加一条记录。注意,这里这个功能和之前的请假流程完全不一样,所以需要一个新的功能,无法复用。最后调用activiti的接口启动一个流程实例。

 第二步:人事查询,这里我们也是直接通过activiti来查询当前用户的任务,完全可以用请假流程的查询。你可以参考下钉钉里面的待我审核,就是一大堆自己的流程任务放在一个页面

第三步:人事提交审核,这里我们需要一个新的页面,虽然都是审核,但数据不同,那就得新页面,然后【审核记录表】增加一条记录,同时调用activiti完成当前节点任务。

 第四步:部门老大查询,同理,和上面共用查询页面。

第五步:部门老大填写交接记录单,这里要一个新的页面,【离职交接记录表】增加一条记录,同时调用activiti完成当前节点任务。

 第六步:人事查询,同理,和上面共用查询页面。

第七步:人事存档,这里要一个新页面,【离职记录表】增加一条记录,同时调用activiti完成当前节点任务。

现在再让我们来总结下用了activiti后我们需要做哪些工作:

共9个步骤:画流程图、填写请假申请单、查询页面,请假审核页面,请假入档页面,填写离职申请单、离职审核页面、离职交接填写页面,离职归档页面

而我们以前也是9个步骤:请假单填写、请假单查询、请假单审批、请假单归档、离职申请单填写、离职单查询、离职单审批、交接记录填写、离职归档。

其实这是必然的,因为activiti不帮我们处理页面。任务工作流引擎都不会帮我们处理页面。

但是我们用了工作流引擎后,我们主要处理的其实是我们的业务数据,查询功能几乎统一没有变化。流程怎么走也不需要我们管了。就算以后你请假不需要经理审核了,我们也不需要改代码了,只要改了流程图就行,毕竟我们没有直接控制任务怎么走。

相比以前,如果砍了个步骤,你是不是得修改部门老大审核完后的代码,让他直接进入行政归档环节。

很多博客、教学视频都会说,用了工作流后,不管流程怎么变,你都不需要改代码,这其实是错误的,因为我们的流程充满了业务,改了流程环节,必定牵扯到业务,所以肯定会进行代码上的修改的。特别是你新增了步骤,修改了业务数据,这些都是必须要改代码的。当然,如果你每个步骤都是进行同样的操作,只是数据的值不同,就像请假的两次审核,还真是可以做到不管怎么变流程,都不用改代码。

也许你看到这里还是会觉得很难理解,那接下来我们看下activiti的实现原理,可能你就能理解了。

首先我们准备下activiti的环境。

1、数据库,先建一个数据库,我自己用mysql,你们用啥自己看着办。

2、创建一个maven项目,加入依赖

 <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<activiti.version>7.1.0.M2</activiti.version>
<slf4j.version>1.7.30</slf4j.version>
</properties>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- activiti的一堆依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>${activiti.version}</version>
<scope>test</scope>
</dependency> <!-- activiti连接我们数据库的jdbc驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!-- 日志,最好加上 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<!-- activiti是用mybatis来操作数据库的,不加也行,反正activiti自带依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<!-- 数据库连接池,你们用别的也许,这个随便 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.7.0</version>
</dependency>

3、activiti配置文件activiti.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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.xsd"> <!-- 配置数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/activiti_demo?serverTimezone=GMT&amp;useUnicode=true&amp;characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean> <!-- activiti单独运行的processEngine配置,使用单独启动方式,这个processEngineConfiguration名字不能乱改,默认bean的名字是他 -->
<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!--数据源-->
<property name="dataSource" ref="dataSource"></property>
<!--是否生成表结构-->
<property name="databaseSchemaUpdate" value="true"></property>
</bean>
</beans>

4、测试一下activiti有没有启动

package activiti01;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.ProcessEngines; /**
* 生成25张表
* */
public class ActivitiGeneratorTable { public static void main(String[] args) {
ActivitiGeneratorTable activitiGeneratorTable = new ActivitiGeneratorTable();
activitiGeneratorTable.testGenTable1();
} /***
* 测试activiti25张表的生成
* 方法一:
* */
public void testGenTable1(){
// 1、创建一个流程引擎配置类ProcessEngineConfiguration
/**
* 如果processEngineConfiguration的bean名字(activiti.cfg.xml中)改了,要将新名字传入,作为第二个参数
* */
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
// 2、创建一个流程引擎类
ProcessEngine processEngine = configuration.buildProcessEngine();
// 3、输出processEngine
System.out.println(processEngine);
} /***
* 测试activiti25张表的生成
* 方法二:
* */
public void testGenTable2(){
/**
* 这种方法要求activiti的配置文件要再classpath直接路径下,同时文件名为"activiti.cfg.xml",而且流程引擎的bean名字为“processEngineConfiguration”(activiti.cfg.xml中)
* 三者缺一不可
* */
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//输出processEngine
System.out.println(processEngine);
}
}

如果没问题,那你的数据库里面应该会有25张act开头的表

关于各个表的作用,有个博客讲的挺好的   https://blog.csdn.net/hj7jay/article/details/51302829    但是这里的表和我们生成的表会有点差异,这是activiti版本的问题,我用的是activiti7的版本,但核心表都一样

大体分成4类表:

act_ru_*:存放运行时记录的表,里面的数据只会在流程实例运行时才存在,一旦流程实例结束了,相关记录就会被清除。包括流程实例、用户任务、变量等

act_re_*:流程定义表,主要存放流程定义信息,我们部署流程后,便有一个流程定义了

act_ge_*:资源表,主要存放一些流程定义的文件资源,流程变量的序列化内容。

act_hi_*:历史表,主要是存放一些历史记录,包括历史任务、历史流程实例、历史流程变量、历史任务的参与者信息表等等。

先说两个比较重要的概念:流程定义、流程实例

流程定义:也就是我们画好的bpmn文件,bpmn文件其实就是一个以.bpmn结尾的xml格式的文件,定义了这个流程的各个任务怎么走,有什么属性等等。当我们部署流程后,会影响ct_ge_bytearray、act_re_deployment、act_re_procdef三张表。相当于我们的Java类。

流程实例:流程定义只是一个模板,而我们每次启动流程,都会以流程定义作为模板,创建一个流程实例,这个流程实例就相当于java类的实例,不同流程实例间是独立的,不影响的。

activiti有几个很重要的类,我们玩activiti基本都是围绕这几个类来玩的:

新版的activiti里面,FormService和IdentityService已经没了。

说了这么多废话,我们还是回到刚刚的代码来看一下吧。

我们的配置文件上有这么一句:

 <!--是否生成表结构-->
<property name="databaseSchemaUpdate" value="true"></property>

我们在获取ProcessEngine 的时候就会去判断这个更新策略,然后帮我们创建表。

环境有了,我们再看看怎么画bpmn文件吧,前面说过,bpmn文件其实就是.bpmn后缀的一个XML格式的文件。我们一般用eclipse或者idea的插件来画。当然也有其他工具。

eclipse或者idea怎么装插件就自己百度吧,不过我idea搜不到插件,所以我就用eclipse了。

每个控件元素,甚至每一条线,都是可以设置属性的。

如果我们用记事本,或者其他文本编辑器打开,就可以发现其就是xml

我把这个xml放出来,你们可以在直接复制,文件保存后缀.bpmn就行

<?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://www.activiti.org/test">
<process id="holiday" name="请假流程" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="填写请假申请" activiti:assignee="zhangsan"></userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
<userTask id="usertask2" name="部门经理审核" activiti:assignee="lisi"></userTask>
<sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
<userTask id="usertask3" name="总经理审核" activiti:assignee="wangwu"></userTask>
<sequenceFlow id="flow3" sourceRef="usertask2" targetRef="usertask3"></sequenceFlow>
<endEvent id="endevent2" name="End"></endEvent>
<sequenceFlow id="flow6" sourceRef="usertask3" targetRef="endevent2"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_holiday">
<bpmndi:BPMNPlane bpmnElement="holiday" id="BPMNPlane_holiday">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="220.0" y="220.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
<omgdc:Bounds height="55.0" width="105.0" x="350.0" y="210.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
<omgdc:Bounds height="55.0" width="105.0" x="500.0" y="210.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
<omgdc:Bounds height="55.0" width="105.0" x="650.0" y="210.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent2" id="BPMNShape_endevent2">
<omgdc:Bounds height="35.0" width="35.0" x="830.0" y="220.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="255.0" y="237.0"></omgdi:waypoint>
<omgdi:waypoint x="350.0" y="237.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="455.0" y="237.0"></omgdi:waypoint>
<omgdi:waypoint x="500.0" y="237.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="605.0" y="237.0"></omgdi:waypoint>
<omgdi:waypoint x="650.0" y="237.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6">
<omgdi:waypoint x="755.0" y="237.0"></omgdi:waypoint>
<omgdi:waypoint x="830.0" y="237.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

好了,我们再来看部署流程要怎么做:

package activiti01;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment; import java.io.InputStream;
import java.util.zip.ZipInputStream; /**
* 流程部署
* 影响了三张表:
* act_ge_bytearray
* act_re_deployment
* act_re_procdef
* */
public class ActivitiDeployment {
public static void main(String[] args) {
deployment1();
} /**
* 方法一:
* 直接部署bpmn文件
* */
private static void deployment1() {
// 1、创建ProcessEngine流程引擎对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); // 2、得到Repositoryervice实例
RepositoryService repositoryService = processEngine.getRepositoryService(); // 3、进行部署,bpmn文件是一定要的,图片文件可以没有,流程key相同的话,会使用最新部署的流程定义
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("bpmn/holiday.bpmn")
.addClasspathResource("bpmn/holiday.png")
.name("请假申请流程")
.deploy(); // 4、输出部署的信息
System.out.println(deployment.getName());
System.out.println(deployment.getId());
} /**
* 方法二:
* 部署zip压缩包
* */
private static void deployment2() {
// 1、创建ProcessEngine流程引擎对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); // 2、得到Repositoryervice实例
RepositoryService repositoryService = processEngine.getRepositoryService(); // 3、获取压缩文件
InputStream inputStream = ActivitiDeployment.class.getClassLoader().getResourceAsStream("bpmn/holiday.zip"); // 4、创建一个ZipInputStream流
ZipInputStream zipInputStream = new ZipInputStream(inputStream); // 3、进行部署,bpmn文件是一定要的,图片文件可以没有,流程key相同的话,会使用最新部署的流程定义
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.name("请假申请流程")
.deploy(); // 4、输出部署的信息
System.out.println(deployment.getName());
System.out.println(deployment.getId());
}
}

有两种部署方式,自己选一种就好。

另外说一下,bpmn文件是必须存在,图片文件是可以没有的,毕竟bpmn就是我们流程定义的主要的内容,没有了还怎么玩呢。

部署流程会影响三张表act_ge_bytearray、act_re_deployment、act_re_procdef

我们来看看这三张表都有啥东西

其实我们将bpmn文件部署上去后,后面我们每次启动流程实例,activiti都会读取我们的bpmn文件,去获取流程文件,并且解析,这样才能知道下一步要干嘛。

我们刚刚部署了一个流程,那我们接下来看看怎么启动这个流程实例:

package activiti01;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.runtime.ProcessInstance; /**
* 启动流程实例
* 影响的表:
* act_hi_actinst 历史活动信息,包括当前任务
* act_hi_identitylink 历史任务参与者信息,包括当前任务的参与者
* act_hi_procinst 历史流程实例
* act_hi_taskinst 历史任务实例,包括当前任务
* act_ru_execution 流程运行时的活动信息
* act_ru_identitylink 当前流程实例的任务参与者信息
* act_ru_task 当前任务节点的信息
* */
public class ActivitiStartInstance {
public static void main(String[] args) {
// 1、得到processEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到RuntimeService对象
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3、得到流程实例,需要直到流程定义的key,也就是流程process文件的Id,可以在bpmn里面查看,也可以在数据库act_re_procdef找到该流程的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holiday");
// 4、输出相关信息
System.out.println("流程部署id"+processInstance.getDeploymentId());
System.out.println("流程实例id"+processInstance.getProcessInstanceId());
System.out.println("活动id"+processInstance.getActivityId());
}
}

   前面介绍bpmn流程图的时候,我就跟你们说过流程文件的id很重要,后面我们要找流程,基本都是根据这个id(activiti里面称之为key)来查询的。

我们在数据库表中也可以找到:

你可以自己对比下你自己的流程文件。

我们还是看看数据库的信息有什么变化先:

启动一个流程时,act_ru_execution会有两条记录,一条是流程实例记录,数据基本不会变。另一条是流程实例执行记录,会记录当前流程实例走到哪个节点。所以我们会在上图看到两条记录,我们获取到的流程实例都是第一条记录,也就是PROC_INST_ID_的那条记录

这里历史任务表,他会记录流程实例所经历过的所有任务,也包括自己当前正在执行的任务。同样,所有历史表也都是这样的,都会记录以前执行过的 + 当前正在执行的任务

这个历史任务表和act_ru_task相对应

这个表和act_ru_execution相对应

这个表和act_ru_identitylink相对应

这个历史表记录了流程经历过的每一个节点,和我们前面act_hi_taskinst、act_ru_task记录的不太一样

这三个表关注的重点不同:

act_ru_task:这个表是流程推进比较关键的一个表,他会记录我们这个流程实例究竟走到哪一步了,他只会记录当前正要执行的任务节点,未开始的节点不会出现,已经结束的节点会被删除。因为这个机制,所以当我们想知道流程走到哪了,只需要查这个表就行了。

act_hi_taskinst:这个表只记录我们已经执行过的任务和当前正要执行的任务。他只关心任务节点,至于其他的流程节点,如开始节点、结束节点、网关节点等等,他都不管。如果你只想知道流程都走过哪些任务,什么时候执行,那查这个表就够了。

act_hi_actinst:这个表记录的比较详细,他包括act_hi_taskinst的记录,以及流程节点的执行记录。如果我们想知道这个流程完整的执行情况,就需要查这个表。

启动了流程,那就来查一下用户的任务吧

package activiti01;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.TaskService;
import org.activiti.engine.task.Task; import java.util.List; /**
* 查询当前用户的任务列表
* */
public class ActivitiTaskQuery {
public static void main(String[] args) {
// 1、得到processEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到TaskService对象
TaskService taskService = processEngine.getTaskService();
// 3、用流程定义的key和负责人assignee来实现当前用户的任务列表查询
List<Task> taskList = taskService.createTaskQuery()
// .processDefinitionKey("holiday4")
.taskAssignee("zhangsan")
.list();
// 4、任务列表查询
for (Task task : taskList) {
System.err.println("流程实例id ==> "+task.getProcessInstanceId());
System.err.println("任务定义key ==> "+task.getTaskDefinitionKey());
System.err.println("任务id ==> "+task.getId());
System.err.println("任务处理人 ==> "+task.getAssignee());
System.err.println("任务名 ==> "+task.getName());
}
}
}
TaskService 提供了很多查询任务的api,可以很方便的进行查询
给你们看看上面那段taskService的查询sql

不知道你们还记不记得我前面跟你们说过,activiti查某个人的任务,只需要用自己的用户标识去调activiti的api就行了,不需要为每个流程都写一套查询方法。看到这里你们应该明白了吧。

当然,如果你有追求一点,也可以为每个流程都进行一下个性化查询调整,反正activiti的api执行挺多的查询条件。

任务查完了,那就来完成任务吧

package activiti01;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.TaskService;
import org.activiti.engine.task.Task; import java.util.List; /**
* 处理用户任务
* 影响的表:
* act_hi_actinst
* act_hi_identitylink
* act_hi_taskinst
* act_ru_identitylink
* act_ru_task
* 如果流程结束了,ru的表里面数据将会被全部清掉
* */
public class ActivitiTackComplete {
public static void main(String[] args) {
// 1、得到processEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到TaskService对象
TaskService taskService = processEngine.getTaskService();
// 3、结合任务查询,将查询到的任务进行处理
List<Task> taskList = taskService.createTaskQuery()
// .processDefinitionKey("holiday")
.taskAssignee("zhangsan")
.list();
// 4、完成任务
for (Task task : taskList) {
taskService.complete(task.getId());
System.err.println(task.getName());
}
}
}

我们再来看看控制台打印的sql,我把多余的都删了

 --- starting CompleteTaskCmd --------------------------------------------------------
==> Preparing: select * from ACT_RU_TASK where ID_ = ?
==> Parameters: 2505(String)
<== Total: 1
==> Preparing: select * from ACT_RE_PROCDEF where ID_ = ?
==> Parameters: holiday:1:4(String)
<== Total: 1
==> Preparing: select * from ACT_RE_DEPLOYMENT where ID_ = ?
==> Parameters: 1(String)
<== Total: 1
Processing deployment 请假申请流程
==> Preparing: select * from ACT_GE_BYTEARRAY where DEPLOYMENT_ID_ = ? order by NAME_ asc
==> Parameters: 1(String)
<== Total: 2
Processing BPMN resource bpmn/holiday.bpmn
Parsing process holiday
==> Preparing: select * from ACT_RE_PROCDEF where DEPLOYMENT_ID_ = ? and KEY_ = ? and (TENANT_ID_ = '' or TENANT_ID_ is null)
==> Parameters: 1(String), holiday(String)
<== Total: 1 --- starting GetProcessDefinitionInfoCmd --------------------------------------------------------
==> Preparing: select * from ACT_PROCDEF_INFO where PROC_DEF_ID_ = ?
==> Parameters: holiday:1:4(String)
<== Total: 0
--- GetProcessDefinitionInfoCmd finished -------------------------------------------------------- ==> Preparing: select * from ACT_RU_TASK where PARENT_TASK_ID_ = ?
==> Parameters: 2505(String)
<== Total: 0
==> Preparing: select * from ACT_RU_IDENTITYLINK where TASK_ID_ = ?
==> Parameters: 2505(String)
<== Total: 0
==> Preparing: select * from ACT_RU_VARIABLE where TASK_ID_ = ?
==> Parameters: 2505(String)
<== Total: 0
Current history level: AUDIT, level required: AUDIT
==> Preparing: select * from ACT_HI_TASKINST where ID_ = ?
==> Parameters: 2505(String)
<== Total: 1
==> Preparing: select E.*, S.PROC_INST_ID_ AS PARENT_PROC_INST_ID_ from ACT_RU_EXECUTION E LEFT OUTER JOIN ACT_RU_EXECUTION S ON E.SUPER_EXEC_ = S.ID_ where E.ID_ = ?
==> Parameters: 2502(String)
<== Total: 1
==> Preparing: select distinct T.* from ACT_RU_TASK T where T.EXECUTION_ID_ = ?
==> Parameters: 2502(String)
<== Total: 1
==> Preparing: select * from ACT_HI_ACTINST RES where EXECUTION_ID_ = ? and ACT_ID_ = ? and END_TIME_ is null
==> Parameters: 2502(String), usertask1(String)
<== Total: 1 --- starting GetNextIdBlockCmd --------------------------------------------------------
==> Preparing: select * from ACT_GE_PROPERTY where NAME_ = ?
==> Parameters: next.dbid(String)
<== Total: 1
==> Preparing: update ACT_GE_PROPERTY SET REV_ = ?, VALUE_ = ? where NAME_ = ? and REV_ = ?
==> Parameters: 4(Integer), 7501(String), next.dbid(String), 3(Integer)
<== Updates: 1
--- GetNextIdBlockCmd finished -------------------------------------------------------- ==> Preparing: select E.*, S.PROC_INST_ID_ AS PARENT_PROC_INST_ID_ from ACT_RU_EXECUTION E LEFT OUTER JOIN ACT_RU_EXECUTION S ON E.SUPER_EXEC_ = S.ID_ where E.ID_ = ?
==> Parameters: 2501(String)
<== Total: 1
==> Preparing: select * from ACT_RU_IDENTITYLINK where PROC_INST_ID_ = ?
==> Parameters: 2501(String)
<== Total: 1 inserting: org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl@ae202c6
==> Preparing: insert into ACT_HI_TASKINST ( ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, OWNER_, ASSIGNEE_, START_TIME_, CLAIM_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TASK_DEF_KEY_, FORM_KEY_, PRIORITY_, DUE_DATE_, CATEGORY_, TENANT_ID_ ) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 5002(String), holiday:1:4(String), 2501(String), 2502(String), 部门经理审核(String), null, null, null, lisi(String), 2020-06-25 13:38:49.636(Timestamp), null, null, null, null, usertask2(String), null, 50(Integer), null, null, (String)
<== Updates: 1 inserting: HistoricActivityInstanceEntity[id=5001, activityId=usertask2, activityName=部门经理审核]
==> Preparing: insert into ACT_HI_ACTINST ( ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 5001(String), holiday:1:4(String), 2501(String), 2502(String), usertask2(String), 5002(String), null, 部门经理审核(String), userTask(String), lisi(String), 2020-06-25 13:38:49.615(Timestamp), null, null, null, (String)
<== Updates: 1 inserting: org.activiti.engine.impl.persistence.entity.HistoricIdentityLinkEntityImpl@2a869a16
==> Preparing: insert into ACT_HI_IDENTITYLINK (ID_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_) values (?, ?, ?, ?, ?, ?)
==> Parameters: 5003(String), participant(String), lisi(String), null, null, 2501(String)
<== Updates: 1 inserting: Task[id=5002, name=部门经理审核]
==> Preparing: insert into ACT_RU_TASK (ID_, REV_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, PRIORITY_, CREATE_TIME_, OWNER_, ASSIGNEE_, DELEGATION_, EXECUTION_ID_, PROC_INST_ID_, PROC_DEF_ID_, TASK_DEF_KEY_, DUE_DATE_, CATEGORY_, SUSPENSION_STATE_, TENANT_ID_, FORM_KEY_, CLAIM_TIME_) values (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 5002(String), 部门经理审核(String), null, null, 50(Integer), 2020-06-25 13:38:49.615(Timestamp), null, lisi(String), null, 2502(String), 2501(String), holiday:1:4(String), usertask2(String), null, null, 1(Integer), (String), null, null
<== Updates: 1 inserting: IdentityLinkEntity[id=5003, type=participant, userId=lisi, processInstanceId=2501]
==> Preparing: insert into ACT_RU_IDENTITYLINK (ID_, REV_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_, PROC_DEF_ID_) values (?, 1, ?, ?, ?, ?, ?, ?)
==> Parameters: 5003(String), participant(String), lisi(String), null, null, 2501(String), null
<== Updates: 1 updating: Execution[ id '2502' ] - activity 'usertask2 - parent '2501'
==> Preparing: update ACT_RU_EXECUTION set REV_ = ?, BUSINESS_KEY_ = ?, PROC_DEF_ID_ = ?, ACT_ID_ = ?, IS_ACTIVE_ = ?, IS_CONCURRENT_ = ?, IS_SCOPE_ = ?, IS_EVENT_SCOPE_ = ?, IS_MI_ROOT_ = ?, PARENT_ID_ = ?, SUPER_EXEC_ = ?, ROOT_PROC_INST_ID_ = ?, SUSPENSION_STATE_ = ?, NAME_ = ?, IS_COUNT_ENABLED_ = ?, EVT_SUBSCR_COUNT_ = ?, TASK_COUNT_ = ?, JOB_COUNT_ = ?, TIMER_JOB_COUNT_ = ?, SUSP_JOB_COUNT_ = ?, DEADLETTER_JOB_COUNT_ = ?, VAR_COUNT_ = ?, ID_LINK_COUNT_ = ? where ID_ = ? and REV_ = ?
==> Parameters: 2(Integer), null, holiday:1:4(String), usertask2(String), true(Boolean), false(Boolean), false(Boolean), false(Boolean), false(Boolean), 2501(String), null, 2501(String), 1(Integer), null, false(Boolean), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 2502(String), 1(Integer)
<== Updates: 1 updating: HistoricActivityInstanceEntity[id=2504, activityId=usertask1, activityName=填写请假申请]
==> Preparing: update ACT_HI_ACTINST set EXECUTION_ID_ = ?, ASSIGNEE_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ? where ID_ = ?
==> Parameters: 2502(String), zhangsan(String), 2020-06-25 13:38:49.552(Timestamp), 227019(Long), null, 2504(String)
<== Updates: 1 updating: org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl@46aa712c
==> Preparing: update ACT_HI_TASKINST set PROC_DEF_ID_ = ?, EXECUTION_ID_ = ?, NAME_ = ?, PARENT_TASK_ID_ = ?, DESCRIPTION_ = ?, OWNER_ = ?, ASSIGNEE_ = ?, CLAIM_TIME_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ?, TASK_DEF_KEY_ = ?, FORM_KEY_ = ?, PRIORITY_ = ?, DUE_DATE_ = ?, CATEGORY_ = ? where ID_ = ?
==> Parameters: holiday:1:4(String), 2502(String), 填写请假申请(String), null, null, null, zhangsan(String), null, 2020-06-25 13:38:49.516(Timestamp), 226968(Long), null, usertask1(String), null, 50(Integer), null, null, 2505(String)
<== Updates: 1 ==> Preparing: delete from ACT_RU_TASK where ID_ = ? and REV_ = ?
==> Parameters: 2505(String), 1(Integer)
<== Updates: 1
--- CompleteTaskCmd finished -------------------------------------------------------- 填写请假申请
Disconnected from the target VM, address: '127.0.0.1:60950', transport: 'socket' Process finished with exit code 0

我们来分析下日志:

starting CompleteTaskCmd
  starting GetProcessDefinitionInfoCmd
  GetProcessDefinitionInfoCmd finished
  
  starting GetNextIdBlockCmd
  GetNextIdBlockCmd finished
CompleteTaskCmd finished
 整体步骤就这样。继续详细分析

想必数据库表的数据变化,不需要我带你们看吧

到目前为止,基本的使用就讲完了。

我们来总结下:

ProcessEngineConfiguration类:
  主要作用是加载activiti.cfg.xml配置文件。

ProcessEngine类:
  帮助我们快速得到各个service接口,并生成activiti的工作环境(也就是25张表)

Service接口:
  可以快速帮我们实现数据库25张表的操作
  RepositoryService
  RuntimeService
  TaskService
  HistoryService

使用步骤:
  1、bpmn文件设计插件安装
  2、画流程图
  3、流程部署:
    方法一:通过bpmn文件部署
    方法二:通过zip压缩包部署
    主要通过RepositoryService来实现
  4、启动流程实例:
    主要通过RuntimeService来实现
  5、查看用户有哪些任务:
    主要通过TaskService来实现
  6、完成任务:
    主要通过TaskService来实现

activiti学习笔记一的更多相关文章

  1. Activiti 学习笔记记录(2016-8-31)

    上一篇:Activiti 学习笔记记录(二) 导读:上一篇学习了bpmn 画图的常用图形标记.那如何用它们组成一个可用文件呢? 我们知道 bpmn 其实是一个xml 文件

  2. Activiti 学习笔记记录(二)

    上一篇:Activiti 学习笔记记录 导读:对于工作流引擎的使用,我们都知道,需要一个业务事件,比如请假,它会去走一个流程(提交申请->领导审批---(批,不批)---->结束),Act ...

  3. Activiti学习笔记目录

    1.Activiti学习笔记1 — 下载与开发环境的配置: 2.Activiti学习笔记2 — HelloWorld: 3.Activiti学习笔记3 — 流程定义: 4.Activiti学习笔记4 ...

  4. Activiti 学习笔记(2016-8-30)

    前言 不管学习什么,都必须对知识体系中专有的名词或者特定的语言组织方式 有所了解. 本文仅记录学习过程中的笔记,参考意义因人而定,不保证所言全部正确. 学习方式,百度传课的视频<权威Activi ...

  5. Activiti 学习笔记记录

    官方在线用户手册(英文版):http://activiti.org/userguide/index.html 中文用户手册:http://www.mossle.com/docs/activiti/in ...

  6. Activiti学习笔记1 — 下载与开发环境的配置

    一.下载 JDK下载URL: Tomcat下载URL:http://tomcat.apache.org/ Eclipse下载URL:http://www.oracle.com/technetwork/ ...

  7. activiti学习笔记

    activiti入门 activiti官网 pom.xml文件 xml <!-- activiti --> <dependency> <groupId>org.ac ...

  8. Activiti学习笔记11 — 判断节点的使用

    一. 创建流程 <?xml version="1.0" encoding="UTF-8"?> <definitions xmlns=" ...

  9. Activiti学习笔记10 — 动态任务分配

    动态任务分配使用的两种方式 一.通过特殊表达式,来获取任务信息 ,在流程 UserTask节点上设置 ${流程变量的Key} 1.流程定义 <?xml version="1.0&quo ...

随机推荐

  1. 【asp.net core 系列】2 控制器与路由的恩怨情仇

    0. 前言 在上一篇文章中,我们初步介绍了asp.net core,以及如何创建一个mvc项目.从这一篇开始,我将为大家展示asp.net core 的各种内容,并且尝试带领大家来挖掘其中的内在逻辑. ...

  2. Alpha冲刺 —— 5.5

    这个作业属于哪个课程 软件工程 这个作业要求在哪里 团队作业第五次--Alpha冲刺 这个作业的目标 Alpha冲刺 作业正文 正文 github链接 项目地址 其他参考文献 无 一.会议内容 1.展 ...

  3. Rocket - diplomacy - resolveStar

    https://mp.weixin.qq.com/s/W1cS9sgwLFjOOm86d05NIA   介绍各类型节点如何确定星型绑定所包含的连接数.     1. 定义   ​​ resoveSta ...

  4. 移动端border:1px问题解决方案

    了解设备像素和css像素的因该知道,通常我们在写移动端时,是按照设计稿标注的像素除以设备的DPR来写真实的像素, 比如在iPhone6上,我们写的20px字体世界上在视觉效应上有20px; 所以当我们 ...

  5. Java实现 LeetCode 590 N叉树的后序遍历(遍历树,迭代法)

    590. N叉树的后序遍历 给定一个 N 叉树,返回其节点值的后序遍历. 例如,给定一个 3叉树 : 返回其后序遍历: [5,6,3,2,4,1]. 说明: 递归法很简单,你可以使用迭代法完成此题吗? ...

  6. Java实现 LeetCode 127 单词接龙

    127. 单词接龙 给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度.转换需遵循如下规则: 每次转换只能改变一个字 ...

  7. Cordova+ionic+angular 项目从 UIWebView 更换为 WKWebView ,通过IOS审核

    当前 cordova-ios 最新版本 5.1.1 新版本 cordova-ios 将删除 UIWebView 代码中的所有引用.WKWebView 将是 Cordova 的默认 Web 视图.   ...

  8. 从程序员到项目主管再到项目总监,一个IT从业者三个职业生涯阶段的工作生活日常

    这是王不留的第 8 篇原创文章 前段时间写过<王不留的十多年工作和生活的流水帐>,在知乎.简书,还有不少微信的朋友私信问我每天四点钟是如何做到的?你现在的作息时间是怎么安排的? 于是,我将 ...

  9. Python 在线免费批量美颜,妈妈再也不用担心我 P 图两小时啦

    引言 首先我承认自己标题党了,我就想提升点阅读量我容易么我,前几天的篇纯技术文阅读量都扯着蛋了. 毕竟阅读量太低实在是没有写下去的动力,我只能用点小手段偶尔提升下阅读量. 这篇文章我转换下套路,先放结 ...

  10. [computer graphics]世界坐标系->相机坐标系详细推导

    基变换 理论部分 在n维的线性空间中,任意n个线性无关的向量都可以作为线性空间的基,即空间基不唯一.对于不同的基,同一个向量的坐标一般是不同的.因为在计算机图形学中,主要研究三维的空间,所以可以简化问 ...