Camunda工作流引擎简记
本文转载自玩转Camunda之实战篇-赶紧收藏起来吧_哔哩哔哩_bilibili
其中部分内容,经过本人修改
一、工作流相关介绍
BPM(BusinessProcessManagement),业务流程管理是一种管理原则,通常也可以代指BPMS(BusinessProcessManagementSuite),是一个实现整合不同系统和数据的流程管理软件套件.
BPMN(BusinessProcessModelandNotation)是基于流程图的通用可视化标准。该流程图被设计用于创建业务流程操作的图形化模型。业务流程模型就是图形化对象的网状图,包括活动和用于定义这些活动执行顺序的流程设计器
。BPMN2.0正式版本于2011年1月3日发布,常见的工作流引擎
如:Activiti、Flowable、jBPM 都基于 BPMN 2.0 标准。
然后来看看BPM的发展历程:
二、Camunda介绍
官网地址:https://camunda.com/
中文站点:http://camunda-cn.shaochenfeng.com/
下载:https://downloads.camunda.cloud/release/camunda-bpm/run/7.15/
案例地址:Congratulation! | docs.camunda.org
前期准备工作: JAVA1.8以上的JRE或JDK
2.1 Camunda Modeler
Camunda Modeler 是Camunda 官方提供的一个流程设计器,用于编辑流程图以及其他模型【表单】,也就是一个流程图的绘图工具。可以官方下载,也可以在提供给大家的资料中获取。获取后直接解压缩即可,注意:解压安装到非中文目录中!!!
启动的效果:
2.2 Camunda BPM
下载地址 https://camunda.com/download/
Camunda BPM 是Camunda官方提供的一个业务流程管理
平台,用来管理,部署的流程定义、执行任务,策略等。下载安装一个Camunda平台,成功解压 Camunda 平台的发行版后,执行名为start.bat(对于 Windows 用户)或start.sh(对于 Unix 用户)的脚本。此脚本将启动应用程序服务器。
打开您的 Web 浏览器并导航到http://localhost:8080/
以访问欢迎页面,Camunda的管理平台。
登录成功的主页:
2.3 入门案例
创建简单流程
我们先通过 Modeler 来绘制一个简单流程
1.) 创建流程:选择 BPMN diagram (Camunda Platform)
2.) 创建开始节点:并设定节点名称
3.) 创建服务节点:设置处理方式
我们切换节点的类型为 service Task
然后我们需要配置刷卡付款
节点,服务类型有很多执行的方法,这次我们使用“external(外部)”任务模式。
具体配置内容为
4.) 添加结束节点
5.) 配置流程参数
点击画布的空白处,右侧的面板会显示当前流程本身的参数,这里我们修改id为payment-retrieval,id是区分流程的标识然后修改Name 为“付款流程”最后确保 Executable
是勾选的,只有Executable
被勾选,流程才能执行
外部任务
在上面设计的流程图,刷卡付款
节点的处理是外部任务,Camunda 可以使多种语言实现业务逻辑,我们以Java为例来介绍。
添加相关的依赖:
<dependencies><dependency><groupId>org.camunda.bpm</groupId><artifactId>camunda-external-task-client</artifactId><version>${camunda.external-task-client.version}</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.36</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency>
</dependencies>
编写处理的业务逻辑的代码
import org.camunda.bpm.client.ExternalTaskClient;import java.awt.*;
import java.net.URI;public class Demo01 {public static void main(String[] args) {ExternalTaskClient client = ExternalTaskClient.create().baseUrl("http://localhost:8080/engine-rest").asyncResponseTimeout(10000) // 长轮询超时时间.build();// 订阅指定的外部任务client.subscribe("charge-card").lockDuration(1000).handler(((externalTask, externalTaskService) -> {// 获取流程变量String item = (String) externalTask.getVariable("item");Long amount = (Long) externalTask.getVariable("amount");System.out.println("item--->"+item + " amount-->" + amount);try {Desktop.getDesktop().browse(new URI("https://docs.camunda.org/get-started/quick-start/complete"));} catch (Exception e) {e.printStackTrace();}// 完成任务externalTaskService.complete(externalTask);})).open();}
}
运行该方法即可,当流程处理时会执行相关逻辑。
部署流程
接下来我们就可以来部署上面定义的流程了。使用 Camunda Modeler 部署流程,点击工具栏中的部署按钮可以将当前流程部署到流程引擎,点击部署按钮,输入Deployment Name 为 “Payment” ,输入下方REST Endpoint 为http://localhost:8080/engine-rest ,然后点击右下角Deploy部署
部署操作:
部署的时候报错:原因是安装路径中有中文
部署成功:
然后在BPM中我们可以查看部署的流程:
启动流程
我们使用Rest API发起流程,所以需要一个接口测试工具(例如:Postman),或者也可以使用电脑自带的curl
curl执行如下命令
curl -H "Content-Type: application/json" -X POST -d '{"variables": {"amount": {"value":555,"type":"long"}, "item": {"value":"item-xyz"} } }' http://localhost:8080/engine-rest/process-definition/key/payment-retrieval/start
postman方式处理
在url中输入:http://localhost:8080/engine-rest/process-definition/key/payment-retrieval/start 通过POST
方式提交,提交的方式是JSON
数据,具体内容为:
{"variables": {"amount": {"value":555,"type":"long"},"item": {"value": "item-xyz"}}
}
然后我们点击发送,操作成功可以看到如下的返回信息
同时任务执行后我们在控制台可以看到相关的信息
三、案例扩展
上面的案例过于简单,我们添加不同的任务节点和网关来丰富下
3.1 用户任务
添加节点
我们在上面的案例中添加一个用户任务
来处理流程。
点击刚刚创建的批准付款
节点,然后通过扳手设置节点的类型为用户任务
(User Task)
然后设置节点的审批人为demo
配置表单
在用户节点
处我们绑定表单数据。然后创建表单相关的字段,并添加对应的描述信息。
通过File > New File > Form创建一个form, 并且以 payment-form
作为该form的编号。
三个字段
- 金额
- Type: Number
- Key: amount
- Field Label: Amount
- 物品
- Type: Text Field
- Key: item
- Field Label: Item
- 是否审批
- Type: Checkbox
- Key: approved
- Label: Approved?
部署流程
流程定义好之后我们就可以部署流程了。直接在Camunda Modeler
工具栏上的上传按钮将流程上传到流程引擎中。部署后在Camunda Web
中查看部署的流程。
测试流程
打开任务列表(http://localhost:8080/camunda/app/tasklist/
),使用 demo / demo 登录。点击右上角的 Start process
,在弹出的对话框中选择“付款流程”.
这时会弹出编辑流程变量的对话框,可以通过点击 Add a variable 按钮添加变量,这次我们先不添加,直接点击右下角Start
启动流程。
这时,在任务列表应该就能看到刚创建的人工任务了,如果没有可以手动刷新一下
到这儿这个用户任务
节点的人工审核就应该要处理了,我们在下一个案例中继续这个案例,我们加入排他网关来处理。
3.2 排他网关
我们将使用排他网关(Exclusive Gateways),为流程添加分支,仅在金额足够大的时候进行人工审核.
添加网关节点
首先打开Camunda Modeler
,在左侧的工具架中找到网关(菱形),将它拖动到“付款请求”和“刷卡付款”之间,将“批准付款”向下移动再添加一个网关,调整流程,最后看起来类似这样:
配置网关
接下来,我们选择“<1000”的连线,打开属性面板,选择“Condition Type”为“Expression”,这里我们使用JAVA统一表达式语言编写条件,这里要做的是在金额小于1000时直接刷卡付款,则输入表达式 ${amount<1000}
同样的,对另一条线也进行配置,表达式为${amount>=1000}
然后是否批准
的排他网关节点我们也需要处理下
部署流程
部署流程和上面的操作是一样的。
测试操作
打开任务列表(http://localhost:8080/camunda/app/tasklist/
),使用 demo / demo 登录,点击右上角的 Start process ,在弹出的对话框中选择“付款流程”,上面例子中,我们直接点击 Start,但这次我们要增加几个变量来测试动态的流程。
试着更改 amount
的值,查看对流程执行顺序的影响
3.3 决策自动化
在上面的案例中我们在审批时是通过用户任务
结合表单来做的审批,本案例我们来看看我们通过DMN
为流程添加一个业务规则来处理
添加业务规则
打开 Camunda Modeler,点击 “批准付款”,在右面的扳手菜单中,将类型改为“Business Rule Task ”(业务规则任务)
下一步,将属性面板中的 Implementation
选择为DMN
,输入 Decision Ref 为 approve-payment
,为了将决策的结果存入流程变量,我们还需要编辑结果变量Result Variable 为approved
,结果类型 Map Decision Result 选择为 singleEntry
,最后结果如下:
创建DMN表
点击 File > New File > DMN Diagram
创建一个新的DMN,现在画布上已经存在一个决策元素了,选择它,然后在右侧属性面板中更改Id
和Name
,这里的Id需要和流程中的Decision Ref
属性一致,这次我们输入Id为approve-payment
接下来,点击决策元素左上角的表格按钮,创建新的DMN表.
编辑DMN表
首先编辑输入参数,在本例中,为了简单,我们依据项目名进行判断,规则可以使用 FEEL 表达式
、JUEL
或者 Script
书写,详情可以阅读 https://docs.camunda.org/manual/latest/user-guide/dmn-engine/expressions-and-scripts/
双击表格中的Input,编辑第一个输入参数
下面,设置输入参数,双击Output编辑
下面,我们点击左侧的蓝色加号,添加一些规则,最后类似这样:
部署DMN表
点击底部的部署按钮,将DMN部署到流程引擎中
流程案例测试
现在打开 http://localhost:8080/camunda/app/cockpit/ ,使用demo/demo登录,可以看到决策定义增加了一个,点击进去可以看到刚才编辑的DMN.
点击进去可以看到对应的决策信息
然后我们按照之前的操作,多启动几个流程,再去看具体的决策结果即可
四、IDEA引入流程设计器
在工作流引擎中流程设计器是一个非常重要的组件,而InterlliJ IDEA
是Java程序员用到的最多的编程工具了。前面在基础篇的介绍中我们都在通过Camunda提供的流程设计器绘制好流程图,然后需要单独的拷贝到项目中,要是调整修改不是很方便,这时我们可以在IDEA中和流程设计器绑定起来。这样会更加的灵活。
4.1 下载Camunda Model
第一步肯定是需要下载Camunda Model
这个流程设计器,我们前面有介绍。就是之前解压好的目录了。
4.2 IDEA中配置
我们先进入settings
中然后找到tools
,继续找到External Tool
.
最终效果
4.3 编辑bpmn文件
找到您想打开的bpmn文件, 点击右键, 找到External Tools 运行camunda modler即可进行文件编写.
搞定~
五、SpringBoot整合Camunda
5.1 官方案例说明
接下来我们看看怎么在我们的实际项目中来使用Camunda了。方式有多种,首先我们可以参考官网提供的整合案例。
但是这里有个比较头疼的问题就是Camunda和SpringBoot版本的兼容性问题,虽然官方也给出了兼容版本的对照表。
但是如果不小心还是会出现各种问题,比如:
上面就是典型的版本不兼容的问题了。
5.2 官方Demo
为了能让我们的案例快速搞定,我们可以通过Camunda官方提供的网站来创建我们的案例程序。地址:https://start.camunda.com/
生成代码后,解压后我们通过idea打开项目,项目结构
相关的pom.xml中的依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.boge.workflow</groupId><artifactId>camunda-project-demo</artifactId><version>1.0.0-SNAPSHOT</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.4.3</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.camunda.bpm</groupId><artifactId>camunda-bom</artifactId><version>7.15.0</version><scope>import</scope><type>pom</type></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-rest</artifactId></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId></dependency><dependency><groupId>org.camunda.bpm</groupId><artifactId>camunda-engine-plugin-spin</artifactId></dependency><dependency><groupId>org.camunda.spin</groupId><artifactId>camunda-spin-dataformat-all</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.4.3</version></plugin></plugins></build></project>
属性文件的配置信息
spring.datasource.url: jdbc:h2:file:./camunda-h2-databasecamunda.bpm.admin-user:id: demopassword: demo
然后通过启动类启动程序
访问服务:http://localhost:8080/
5.3 MySQL数据库
上面的例子我们数据存储在了H2这个内存型数据库,我们可以切换到MySQL
数据库。首先我们需要导入相关的SQL脚本。位置就在我们之前下载的Camunda Web
服务中。
执行创建所有必需的表和默认索引的SQL DDL脚本。上面两个脚本都要执行。
生成的相关表结构比较多,因为本身就是基于Activiti演变而来,所以有Activiti基础的小伙伴会非常轻松了。简单介绍下相关表结构的作用。
- ACT_RE :'RE’表示 repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。
- ACT_RU:'RU’表示 runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Flowable只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。
- ACT_HI:'HI’表示 history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。
- ACT_GE: GE 表示 general。 通用数据, 用于不同场景下
- ACT_ID: ’ID’表示identity(组织机构)。这些表包含标识的信息,如用户,用户组,等等。
具体的表结构的含义:
表分类 | 表名 | 解释 |
---|---|---|
一般数据 | ||
[ACT_GE_BYTEARRAY] | 通用的流程定义和流程资源 | |
[ACT_GE_PROPERTY] | 系统相关属性 | |
流程历史记录 | ||
[ACT_HI_ACTINST] | 历史的流程实例 | |
[ACT_HI_ATTACHMENT] | 历史的流程附件 | |
[ACT_HI_COMMENT] | 历史的说明性信息 | |
[ACT_HI_DETAIL] | 历史的流程运行中的细节信息 | |
[ACT_HI_IDENTITYLINK] | 历史的流程运行过程中用户关系 | |
[ACT_HI_PROCINST] | 历史的流程实例 | |
[ACT_HI_TASKINST] | 历史的任务实例 | |
[ACT_HI_VARINST] | 历史的流程运行中的变量信息 | |
流程定义表 | ||
[ACT_RE_DEPLOYMENT] | 部署单元信息 | |
[ACT_RE_MODEL] | 模型信息 | |
[ACT_RE_PROCDEF] | 已部署的流程定义 | |
运行实例表 | ||
[ACT_RU_EVENT_SUBSCR] | 运行时事件 | |
[ACT_RU_EXECUTION] | 运行时流程执行实例 | |
[ACT_RU_IDENTITYLINK] | 运行时用户关系信息,存储任务节点与参与者的相关信息 | |
[ACT_RU_JOB] | 运行时作业 | |
[ACT_RU_TASK] | 运行时任务 | |
[ACT_RU_VARIABLE] | 运行时变量表 | |
用户用户组表 | ||
[ACT_ID_BYTEARRAY] | 二进制数据表 | |
[ACT_ID_GROUP] | 用户组信息表 | |
[ACT_ID_INFO] | 用户信息详情表 | |
[ACT_ID_MEMBERSHIP] | 人与组关系表 | |
[ACT_ID_PRIV] | 权限表 | |
[ACT_ID_PRIV_MAPPING] | 用户或组权限关系表 | |
[ACT_ID_PROPERTY] | 属性表 | |
[ACT_ID_TOKEN] | 记录用户的token信息 | |
[ACT_ID_USER] | 用户表 |
然后我们在SpringBoot项目中导入MySql
的依赖,然后修改对应的配置信息
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
修改application.yaml
。添加数据源的相关信息。
# spring.datasource.url: jdbc:h2:file:./camunda-h2-databasecamunda.bpm.admin-user:id: demopassword: demo
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/camunda1?serverTimezone=Asia/Shanghaiusername: rootpassword: 123456
camunda:bpm:database:type: mysqlschema-update: trueauto-deployment-enabled: false # 自动部署 resources 下的 bpmn文件
然后启动项目,发现数据库中有了相关记录,说明操作成功
六、Camunda专题讲解
用了整合的基础我们就可以来完成一个流程审批的案例了
6.1 部署流程
@RestController
@RequestMapping("/flow")
public class FlowController {@Autowiredprivate RepositoryService repositoryService;@GetMapping("/deploy")public String deplopy(){Deployment deploy = repositoryService.createDeployment().name("部署的第一个流程") // 定义部署文件的名称.addClasspathResource("process.bpmn") // 绑定需要部署的流程文件.deploy();// 部署流程return deploy.getId() + ":" + deploy.getName();}
}
启动后访问接口即可
6.2 启动流程
启动流程我们通过单元测试来操作
package com.boge.workflow;import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest(classes = Application.class)
public class ApplicationTest {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;/*** 启动流程的案例*/@Testpublic void startFlow(){// 部署流程ProcessInstance processInstance = runtimeService.startProcessInstanceById("1a880f27-2e57-11ed-80d9-c03c59ad2248");// 部署的流程实例的相关信息System.out.println("processInstance.getId() = " + processInstance.getId());System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId());}}
6.3 查询待办
查询待办也就是查看当前需要审批的任务,通过TaskService来处理
/*** 查询任务* 待办** 流程定义ID:processDefinition : 我们部署流程的时候会,每一个流程都会产生一个流程定义ID* 流程实例ID:processInstance :我们启动流程实例的时候,会产生一个流程实例ID*/@Testpublic void queryTask(){List<Task> list = taskService.createTaskQuery()//.processInstanceId("eff78817-2e58-11ed-aa3f-c03c59ad2248").taskAssignee("demo1").list();if(list != null && list.size() > 0){for (Task task : list) {System.out.println("task.getId() = " + task.getId());System.out.println("task.getAssignee() = " + task.getAssignee());}}}
6.4 完成任务
/*** 完成任务*/@Testpublic void completeTask(){// 根据用户找到关联的TaskTask task = taskService.createTaskQuery()//.processInstanceId("eff78817-2e58-11ed-aa3f-c03c59ad2248").taskAssignee("demo").singleResult();if(task != null ){taskService.complete(task.getId());System.out.println("任务审批完成...");}}
七、参考
- Modeler Download Center
- Run Download Center
- Quick Start Camunda | docs.camunda.org
- Camunda Platform Initializr
Camunda工作流引擎简记相关推荐
- camunda工作流引擎流程定义部署 流程定义查询 激活流程实例
camunda工作流引擎流程定义部署 流程定义查询 激活流程实例 1.通过xml字符串部署流程定义 /*** 通过xml字符串部署流程定义* @param processModelVo* @retur ...
- Camunda工作流引擎三
本篇继续拓宽对 Camunda 工作流的学习! <Camunda 工作流引擎一> <Camunda 工作流引擎二> 提要 文章目录 提要 正文 组任务 网关 排他网关 并行网关 ...
- Camunda 工作流引擎 demo
CamundaDemo 本Demo采用springBoot和camunda内嵌的方式. 在SpringBoot中加入ca ...
- Camunda工作流引擎一
实习工作中需要用到工作流引擎,去实现业务审批流的功能模块,由于 Flowable 不支持 MariaDB (重要原因之一),所以项目中选择了 Camunda 工作流引擎. 由于没有接触过工作流引擎,所 ...
- Camunda工作流引擎之bpmn设计器定制
在 Vue.js应用中,基于 bpmn-js. 定制 vue-bpmn-modeler 前段时间,由于公司业务需要工作流引擎,但是由于之前的工作流引擎是第三方付费的,又遇到客户需要本地地方部署,所以需 ...
- OA 系统工作流引擎 Camunda 实践(1)
[审核人员看清楚再审核,我是把自己公司的案例分析一下, 这哪是广告???] Camunda是Activiti最大的贡献者之一(除Alfresco以外),同时也是它一个主 要的执行咨询合作伙伴.cam ...
- 三、Camunda工作流的表和用途说明(实践是检验真理的唯一标准)
本人在工作中用的Camunda7.11版本共47张表. camunda工作流的表大体上分为 5 类: ACT_RE_*: 'RE'表示流程资源存储,这个前缀的表包含了流程定义和流程静态资源(图片,规则 ...
- Camunda工作流平台的使用
工作流可以实现业务流程的自动化,用户可以自己定义工作流程,通过流程来把常用的任务组织起来,而无需在程序中固化流程.这也符合当今微服务,低代码开发的趋势. Camunda是目前主流的一个工作流平台,遵循 ...
- Camunda将工作流引擎引入到微服务领域
今天,工作流程自动化的软件公司Camunda宣布,Zeebe的第一个生产就绪版本现在可以作为免费的社区版下载. Zeebe是一个为云架构而构建的现代工作流引擎,可提供对跨多个微服务的工作流的可见性和控 ...
最新文章
- 毕业设计(二十四)---退出 博客 清除session
- ASP字符转换:UTF-8与GB2312互转
- [系统安全] 二十四.逆向分析之OllyDbg调试INT3断点、反调试、硬件断点与内存断点
- html5 测评游戏,暗黑之王评测:HTML5游戏铸就最华丽ARPG冒险
- 单片机控制直流电机正反转
- Python识别图片中的文字
- python function terminated_Calibre 转换 TXT 文件出现 Errno 21 错误的解决方法 – 书伴
- Leetcode 809. Expressive Words
- 学习笔记:CentOS7学习之十六:LVM管理和ssm存储管理器使用
- 关于MySQL外键的设置
- SpringBoot集成SwaggerUI及其使用
- 180522 安卓-DDCTF2018(RSA)
- BZOJ 4239 巴士走读
- 推荐几个IDEA插件,Java开发者撸码神器。
- 着力抓好服务中心建设,实现网格综治一体化管理
- 搜狗输入法词库php词库怎么用,中州韵输入法导入搜狗词库(示例代码)
- 一文带你详细学习什么是光纤到户FTTH
- Spark Zeppelin
- 感应电机FOC控制及matlab仿真
- 脑壳清痛之---总结thinkphp5 怎么用odbc连接sql2000
热门文章
- linux 例配置文件生效,Linux如何让修改的配置生效?
- Flutter速来系列23-5、NestedScrollVi
- 洛谷c语言题库,洛谷刷题总结
- Java中Map接口的使用和有关集合的面试知识点补充
- 未能找到元数据文件问题
- 引入非线性激励函数的作用
- 从零开始搭建Kafka+SpringBoot分布式消息系统
- Android平台Camera实时滤镜实现方法探讨(九)--磨皮算法探讨(一)
- 骑马钉 双联 竖版 正反印
- 第096封“情书”:十六亩地PDG For Design Work Pt. 3 - Setting Up Distributed PDG<Entagma>Houdini 2019