在前一篇文章的基础上,编写一个稍微复杂的请假流程,进一步熟悉Activiti的基本知识,并期望能够触类旁通,解决上一篇博文中没有解决的问题。实现过程中,参考了以下文章,在此向作者表示感谢。
2、Activiti 6.0 工作流入门 之HelloWorld
另外,程序猿之洞关于Activiti工作流的系列博文也为本文提供了重要的参考信息。
1、功能设计
编写控制台应用程序,创建Activiti流程引擎,创建并部署请假业务流程实例,通过控制台输入,驱动业务流程执行。请假流程如图1所示。
为了熟悉不同类型任务的使用方法,在流程结束前分别加入脚本任务(ScriptTask)和服务任务(ServiceTask)显示审批结果。
2、创建项目
(1)创建Maven项目
项目命名为LeaveProcess,业务类写在src/main/java下,相应的资源文件放置在src/main/resources下。测试的业务类在src/test/java下,相应的测试资源文件放置在src/test/resources下。
(2)配置pom.xml
特别说明的是,此处加入了三个特别的依赖信息:
① guava
Guava是google维护的一种基于开源Java库,此处用于使用【com.google.common.collect.Maps】提供的Maps方法。
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
②jaxb-api
jaxb api是Java EE 的API,默认情况下,Java 9之后,Java SE中将不再包含Java EE 的Jar包。本文中在执行脚本任务时需要这个API,否则会出现如下类似错误:
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException……
参考《How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException in Java 9 》一文,在pom.xml文件中加入如下依赖可解决上述问题。
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
③groovy-all
脚本任务中,由于javascrip不支持任何形式的打印或者输出的函数,因此本文采用groovy脚本编写任务,需要增加支持groovy的依赖。
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.0-alpha-3</version>
<type>pom</type>
</dependency>
有关自动执行任务和Groovy语言的基本知识,可参考《工作流Activiti的学习总结(八)Activiti自动执行的应用》、《Activiti脚本任务(ScriptTask)》和简单的土豆的博文《Groovy 语言快速入门》。
综上,完整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.seu.liuds.activiti</groupId>
<artifactId>LeaveProcess</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>LeaveProcess</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>10.0.0.2</jdk.version>
<junit.version>3.8.1</junit.version>
<activiti.version>6.0.0</activiti.version>
<slf4j.version>1.7.21</slf4j.version>
<mysql.connector.version>8.0.11</mysql.connector.version>
<guava.version>26.0-jre</guava.version>
<jaxb-api.version>2.3.0</jaxb-api.version>
<groovy-all.version>3.0.0-alpha-3</groovy-all.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy-all.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</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>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
</dependencies>
</project>
(3)创建主程序类
Maven工程创建后,src/main/java下包含一个名为App的Java文件,内容为一个包含main函数的Java类,如下所示。这里将以此为基础,构建项目的主程序类。
package com.seu.liuds.activiti.LeaveProcess;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}
(4)创建MySQL数据库
使用MySQLWorkbench作为数据库管理工具,创建名为【leaveprocess】的数据库,用户名为【root】,密码为空。
(5)配置日志记录
在/src/main/resources/中添加log4j.properties文件,设置信息显示级别为DEBUG,文件内容如下:
log4j.rootLogger=DEBUG, ACT
log4j.appender.ACT=org.apache.log4j.ConsoleAppender
log4j.appender.ACT.layout=org.apache.log4j.PatternLayout
log4j.appender.ACT.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n
有关log4j.properties的功能和设定可参考《Maven项目中添加log4j.properties实现日志功能工具》 一文。
3、创建流程引擎
流程引擎的创建过程可以直接在程序中用Java语句配置引擎的相关参数,也可以通过读取配置文件完成,具体方法参考萝卜地里的兔子的博文《Activiti5第八弹,ProcessEngineConfiguration和ProcessEngine》。之前采用了Java语句直接设置引擎参数配置,此处采用xml配置文件创建流程引擎。
(1)创建流程引擎配置文件
创建名为【activiti-cfg.xml】配置文件,存放到【src/main/resources】
<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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- processEngine Configuration -->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="jdbcDriver" value="com.mysql.cj.jdbc.Driver" />
<property name="jdbcUrl"
value="jdbc:mysql://localhost:3306/leaveprocess?useSSL=false&serverTimezone=GMT%2B8" />
<property name="jdbcUsername" value="root" />
<property name="jdbcPassword" value="" />
<property name="databaseSchemaUpdate" value="true" />
</bean>
</beans>
需要注意以下两个问题:
① &符号在 xml 中是作为实体字符形式存在的,因此需要通过html的实体名称来代替。此处&应为【&】,并用【;】分隔。常用的实体名称参见《HTML字符实体引用》一文。
②此前的一个例子在MySQL服务器上创建了一个名为activiti的数据库,新建的名为leaveprocess的数据库在初始化过程中出现了以下错误。
### …… Cause: java.sql.SQLSyntaxErrorException: Table 'leaveprocess.act_ge_property' doesn't exist
### The error may exist in org/activiti/db/mapping/entity/Property.xml
### The error may involve org.activiti.engine.impl.persistence.entity.PropertyEntityImpl.selectProperty-Inline
### The error occurred while setting parameters
### SQL: select * from ACT_GE_PROPERTY where NAME_ = ?
……
参考icezcity的博文《一个MySql实例自动创建多个Activiti数据库问题》,将名为activiti的数据库删除以后解决,具体的技术细节需要进一步学习。
(2)加载配置文件创建流程引擎
在App.java文件中创建方法【getProcessEngine()】,加载【activiti-cfg.xml】创建流程引擎,并在main函数中调用该方法。代码如下。
package com.seu.liuds.activiti.LeaveProcess;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
public class App {
public static void main(String[] args) {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start application-----");
// Create Process Engine
ProcessEngine processEngine = getProcessEngine();
strNowTime = CurrentTime();
System.out.println(strNowTime + " --End application-----");
}
private static ProcessEngine getProcessEngine() {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start building a process engine---");
ProcessEngineConfiguration cfg = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResource("activiti-cfg.xml");
ProcessEngine processEngine = cfg.buildProcessEngine();
String engineName = processEngine.getName();
String engineVersion = ProcessEngine.VERSION;
strNowTime = CurrentTime();
System.out.println(strNowTime + " --Process engine [" + engineName + "] v" + engineVersion + " was built---");
return processEngine;
}
private static String CurrentTime() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// set date format
String strCurrentTime = df.format(new Date());// new Date(): get current system time
return strCurrentTime;
}
}
App.java的运行结果如下:
2018-08-10 14:21:13 --Start application-----
2018-08-10 14:21:13 --Start building a process engine---
……
2018-08-10 14:21:17 --Process engine [default] v6.0.0.4 was built---
2018-08-10 14:21:17 --End application-----
4、部署流程定义文件
(1)创建流程定义文件
使用Activiti BPMN2.0 Designer设计流程定义文件,方法可参见《Activiti学习文档(三)之画流程图并部署流程》一文。由于文件篇幅较大,上传后提供下载链接demo.bpmn20.xml。系统设定下载所需资源最少为1积分/C币,如果需要请在留言里提供邮箱。
① 定义脚本任务
在Activiti Designer中选中脚本任务图标,按照如图2所示设置将要执行的groovy脚本。
②定义服务任务
编写需要执行的任务类,本例中只在控制台输出请假审批信息,代码如下。
package com.seu.liuds.activiti.LeaveProcess;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.JavaDelegate;
public class PrintServiceTask implements JavaDelegate {
private final String taskName = PrintServiceTask.class.getName();
public void execute(DelegateExecution execution) {
System.out.println("variavles=" + execution.getVariables());
execution.setVariable("审批意见", "不同意。");
System.out.println(taskName);
}
}
在Activiti Designer中选中服务任务图标,按照图3所示设置将要执行的类名。
(2)部署流程定义文件
将此前设计好的流程定义文件部署在流程引擎上。在App.java文件中创建方法【getProcessDefinition(ProcessEngine processEngine)】,并在main函数中调用该方法。核心代码如下(限于篇幅,import导入语句和前文已经出现的代码段落不再详细罗列)。
package com.seu.liuds.activiti.LeaveProcess;
……
public class App {
public static void main(String[] args) throws ParseException {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start application-----");
// Create Process Engine
ProcessEngine processEngine = getProcessEngine();
// Deploy Process Definition
ProcessDefinition processDefinition = getProcessDefinition(processEngine);
strNowTime = CurrentTime();
System.out.println(strNowTime + " --End application-----");
}
private static ProcessDefinition getProcessDefinition(ProcessEngine processEngine) {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start deploying process definition file---");
RepositoryService repositoryService = processEngine.getRepositoryService();
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
deploymentBuilder.addClasspathResource("demo.bpmn20.xml");
Deployment deployment = deploymentBuilder.deploy();
String deploymentId = deployment.getId();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deploymentId).singleResult();
String processName = processDefinition.getName();
strNowTime = CurrentTime();
System.out.println(strNowTime + " --Process definition file [" + processName + "] was deployed");
return processDefinition;
}
private static ProcessEngine getProcessEngine() {
……
}
private static String CurrentTime() {
……
}
}
App.java的运行结果如下:
2018-08-10 14:55:59 --Start application-----
2018-08-10 14:55:59 --Start building a process engine---
……
2018-08-10 14:56:02 --Process engine [default] v6.0.0.4 was built---
2018-08-10 14:56:02 --Start deploying process definition file---
2018-08-10 14:56:03 --Process definition file [LeaveApproveProcess] was deployed
2018-08-10 14:56:03 --End application-----
5、启动运行流程
启动已经部署在流程引擎上的流程定义文件。在App.java文件中创建方法【getProcessDefinition(ProcessEngine processEngine)】,并在main函数中调用该方法。核心代码如下。
package com.seu.liuds.activiti.LeaveProcess;
……
public class App {
public static void main(String[] args) throws ParseException {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start application-----");
// Create Process Engine
ProcessEngine processEngine = getProcessEngine();
// Deploy Process Definition
ProcessDefinition processDefinition = getProcessDefinition(processEngine);
// Run the Process
ProcessInstance processInstance = getProcessInstance(processEngine, processDefinition);
strNowTime = CurrentTime();
System.out.println(strNowTime + " --End application-----");
}
private static ProcessInstance getProcessInstance(ProcessEngine processEngine,
ProcessDefinition processDefinition) {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start running process instance---");
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
strNowTime = CurrentTime();
System.out.println(
strNowTime + " --Process instance [" + processInstance.getProcessDefinitionKey() + "] started---");
return processInstance;
}
private static ProcessDefinition getProcessDefinition(ProcessEngine processEngine) {
……
}
private static ProcessEngine getProcessEngine() {
……
}
private static String CurrentTime() {
……
}
}
App.java的运行结果如下:
2018-08-10 15:02:31 --Start application-----
2018-08-10 15:02:31 --Start building a process engine---
……
2018-08-10 15:02:34 --Process engine [default] v6.0.0.4 was built---
2018-08-10 15:02:34 --Start deploying process definition file---
2018-08-10 15:02:35 --Process definition file [LeaveApproveProcess] was deployed
2018-08-10 15:02:35 --Start running process instance---
2018-08-10 15:02:35 --Process instance [leaveApprove] started---
2018-08-10 15:02:35 --End application-----
6、处理流程任务
按照已经运行的流程定义处理各个流程任务,并返回处理结果。本文的例子中,请假的申请和审批都是手工操作,显示请假处理结果则依靠脚本任务和服务任务分别实现。在App.java文件中创建方法【processTask(ProcessEngine processEngine, ProcessInstance processInstance)】,并在main函数中调用该方法。核心代码如下。
package com.seu.liuds.activiti.LeaveProcess;
……
public class App {
public static void main(String[] args) throws ParseException {
String strNowTime = CurrentTime();
System.out.println(strNowTime + " --Start application-----");
// Create Process Engine
ProcessEngine processEngine = getProcessEngine();
// Deploy Process Definition
ProcessDefinition processDefinition = getProcessDefinition(processEngine);
// Run the Process
ProcessInstance processInstance = getProcessInstance(processEngine, processDefinition);
// Deal with Process Tasks
processTask(processEngine, processInstance);
strNowTime = CurrentTime();
System.out.println(strNowTime + " --End application-----");
}
private static void processTask(ProcessEngine processEngine, ProcessInstance processInstance)
throws ParseException {
Scanner scanner = new Scanner(System.in);
while (processInstance != null && !processInstance.isEnded()) {
TaskService taskService = processEngine.getTaskService();
List<Task> list = taskService.createTaskQuery().list();
System.out.println("--Number of to-do tasks: " + list.size());
for (Task task : list) {
System.out.println("--To-do task: " + task.getName());
Map<String, Object> variables = getMap(processEngine, scanner, task);
taskService.complete(task.getId(), variables);
processInstance = processEngine.getRuntimeService().createProcessInstanceQuery()
.processInstanceId(processInstance.getId()).singleResult();
}
}
scanner.close();
}
private static Map<String, Object> getMap(ProcessEngine processEngine, Scanner scanner, Task task)
throws ParseException {
FormService formService = processEngine.getFormService();
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
List<FormProperty> formProperties = taskFormData.getFormProperties();
Map<String, Object> variables = Maps.newHashMap();
for (FormProperty property : formProperties) {
String line = null;
if (StringFormType.class.isInstance(property.getType())) {
System.out.println(">> Please input " + property.getName());
line = scanner.nextLine();
variables.put(property.getId(), line);
} else if (DateFormType.class.isInstance(property.getType())) {
System.out.println(">> Please input " + property.getName() + "(yyyy-MM-dd)");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
while (date == null) {
line = scanner.nextLine();
try {
date = dateFormat.parse(line);
} catch (Exception e) {
System.out.println(">> Please input " + property.getName() + "(yyyy-MM-dd)");
continue;
}
}
variables.put(property.getId(), date);
} else {
System.out.println(">> Incompatible with " + property.getType() + " type");
}
}
return variables;
}
private static ProcessInstance getProcessInstance(ProcessEngine processEngine,
ProcessDefinition processDefinition) {
……
}
private static ProcessDefinition getProcessDefinition(ProcessEngine processEngine) {
……
}
private static ProcessEngine getProcessEngine() {
……
}
private static String CurrentTime() {
……
}
}
运行App.java,按照提示输入信息,查看流程的执行情况。结果如下:
……
--Number of to-do tasks: 1
--To-do task: 请假申请
>> Please input 申请人姓名
Jerry
>> Please input 请假原因
调研
>> Please input 提交时间(yyyy-MM-dd)
2018-08-10
>> Please input 确认申请
Y
--Number of to-do tasks: 1
--To-do task: 部门审批
>> Please input 主管审批结果
Y
>> Please input 主管备注
同意
--Number of to-do tasks: 1
--To-do task: 人事审批
>> Please input 人事审批结果
Y
>> Please input 人事审批备注
同意
申请者姓名:Jerry
请假原因:调研
2018-08-10 15:26:44 --End application-----
当然,输入数据过程中也可以选择其他路径执行。
最后,推荐一篇不错的博文《activiti使用实践案例》。
本文的主程序代码文件下载链接。同上,需要的可以留邮箱。