0%

Flowable-Demo

阅读更多

1 环境

  1. IDEA
  2. Maven3.5.3
  3. Spring-Boot-2.0.4.RELEASE
  4. Flowable-6.3.0

2 工程目录如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.
├── pom.xml
└── src
├── main
│   ├── java
│   │   └── org
│   │   └── liuyehcf
│   │   └── flowable
│   │   ├── Application.java
│   │   ├── config
│   │   │   ├── DataSourceConfig.java
│   │   │   └── ElementAspect.java
│   │   ├── element
│   │   │   ├── DemoListener.java
│   │   │   └── DemoServiceTask.java
│   │   ├── service
│   │   │   └── DemoService.java
│   │   ├── utils
│   │   │   ├── CreateSqlUtils.java
│   │   │   └── UpgradeSqlUtils.java
│   │   └── web
│   │   └── DemoController.java
│   └── resources
│   ├── application.properties
│   ├── logback.xml
│   └── process
│   └── sample.bpmn20.xml
└── test
├── java
│   └── org
│   └── liuyehcf
│   └── flowable
│   └── test
│   ├── DemoTest.java
│   ├── EmbeddedDatabaseConfig.java
│   └── TestApplication.java
└── resources
└── logback-test.xml

3 pom文件

主要依赖项如下

  1. flowable
  2. spring-boot
  3. jdbc
  4. h2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="UTF-8"?>
<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>org.liuyehcf</groupId>
<artifactId>flowable</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!-- flowable -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-basic</artifactId>
<version>6.3.0</version>
</dependency>

<!-- jdbc -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>

<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>

<!-- utility -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

<!-- logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

4 Java源文件

4.1 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.liuyehcf.flowable;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/**
* @author hechenfeng
* @date 2018/7/25
*/
@SpringBootApplication
@ComponentScan(basePackages = "org.liuyehcf.flowable")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

4.2 DataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package org.liuyehcf.flowable.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

/**
* @author hechenfeng
* @date 2018/7/25
*/
@Configuration
public class DataSourceConfig {

@Value("${spring.datasource.url}")
private String url;

@Value("${spring.datasource.username}")
private String username;

@Value("${spring.datasource.password}")
private String password;

@Bean(name = "dataSource")
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource());

return manager;
}

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}

}

4.3 ElementAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.liuyehcf.flowable.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
* @author hechenfeng
* @date 2018/8/17
*/
@Aspect
@Component
public class ElementAspect {
@Around("execution(* org.liuyehcf.flowable.element.*.*(..))")
public Object taskAround(ProceedingJoinPoint proceedingJoinPoint) {

Object[] args = proceedingJoinPoint.getArgs();

try {
return proceedingJoinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

4.4 DemoListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package org.liuyehcf.flowable.element;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.identitylink.api.IdentityLink;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author hechenfeng
* @date 2018/8/18
*/
@Component
@Scope(scopeName = "prototype")
@Slf4j
public class DemoListener implements TaskListener, ExecutionListener {
@Override
public void notify(DelegateExecution execution) {
FlowElement currentFlowElement = execution.getCurrentFlowElement();
log.info("ExecutionListener is trigger. elementId={}", currentFlowElement.getId());
}

@Override
public void notify(DelegateTask delegateTask) {
String taskName = delegateTask.getName();
String assignee = delegateTask.getAssignee();
Set<IdentityLink> candidates = delegateTask.getCandidates();

List<CandidateInfo> candidateInfoList = candidates.stream().map(CandidateInfo::new).collect(Collectors.toList());
log.info("TaskListener is trigger. taskName={}; assignee={}; candidateInfoList={}", taskName, assignee, JSON.toJSON(candidateInfoList));
}

private static final class CandidateInfo {
private final String groupId;
private final String userId;

private CandidateInfo(IdentityLink identityLink) {
this.groupId = identityLink.getGroupId();
this.userId = identityLink.getUserId();
}

public String getGroupId() {
return groupId;
}

public String getUserId() {
return userId;
}
}

}

4.5 DemoServiceTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.liuyehcf.flowable.element;

import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.common.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
* @author hechenfeng
* @date 2018/8/17
*/
@Component
@Scope(scopeName = "prototype")
@Slf4j
public class DemoServiceTask implements JavaDelegate {

private Expression field1;

private Expression field2;

public void setField1(Expression field1) {
this.field1 = field1;
}

@Override
public void execute(DelegateExecution execution) {
if (field1 == null) {
log.error("Filed injection failed. fieldName={}", "field1");
} else {
log.info("Filed injection succeeded. fieldName={}", "field1");
}

if (field2 == null) {
log.error("Filed injection failed. fieldName={}", "field2");
} else {
log.info("Filed injection succeeded. fieldName={}", "field2");
}
}
}

4.6 DemoService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package org.liuyehcf.flowable.service;

import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
* @author hechenfeng
* @date 2018/7/26
*/
@Service
@Slf4j
public class DemoService {

private static final String BPMN_FILE_PATH = "process/sample.bpmn20.xml";

@Autowired
private RepositoryService repositoryService;

@Autowired
private RuntimeService runtimeService;

@Autowired
private TaskService taskService;

public String deployProcess() {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource(BPMN_FILE_PATH)
.deploy();

ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.singleResult();

log.info("Deploy process success! processDefinition={}",
processDefinition.getId(),
processDefinition.getName());

return processDefinition.getId();
}

public String startProcess(String processDefinitionId) {

ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinitionId);

log.info("Start process success! processDefinitionId={}; processInstanceId={}",
processDefinitionId,
processInstance.getId());

return processInstance.getId();
}

public String completeUserTaskByAssignee(String assignee) {
List<Task> taskList = taskService.createTaskQuery().taskAssignee(assignee).list();
return completeTasks(assignee, taskList);
}

public String completeUserTaskByCandidateUser(String candidateUser) {
List<Task> taskList = taskService.createTaskQuery().taskCandidateUser(candidateUser).list();
return completeTasks(candidateUser, taskList);
}

private String completeTasks(String user, List<Task> taskList) {
if (CollectionUtils.isEmpty(taskList)) {
return "user [" + user + "] has no task todo";
}

StringBuilder sb = new StringBuilder();

for (Task task : taskList) {
String taskId = task.getId();
taskService.complete(taskId);
sb.append("task[")
.append(taskId)
.append("] is complete by ")
.append(user)
.append('\n');
}

return sb.toString();
}
}

4.7 CreateSqlUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package org.liuyehcf.flowable.utils;

import org.apache.commons.io.IOUtils;

import java.io.*;
import java.util.Arrays;
import java.util.List;

/**
* @author hechenfeng
* @date 2018/8/18
*/
public class CreateSqlUtils {

private static final List<String> SQL_PATH_LIST = Arrays.asList(
"org/flowable/common/db/create/flowable.mysql.create.common.sql",

"org/flowable/idm/db/create/flowable.mysql.create.identity.sql",
"org/flowable/identitylink/service/db/create/flowable.mysql.create.identitylink.sql",
"org/flowable/identitylink/service/db/create/flowable.mysql.create.identitylink.history.sql",

"org/flowable/variable/service/db/create/flowable.mysql.create.variable.sql",
"org/flowable/variable/service/db/create/flowable.mysql.create.variable.history.sql",
"org/flowable/job/service/db/create/flowable.mysql.create.job.sql",
"org/flowable/task/service/db/create/flowable.mysql.create.task.sql",
"org/flowable/task/service/db/create/flowable.mysql.create.task.history.sql",

"org/flowable/db/create/flowable.mysql.create.engine.sql",

"org/flowable/db/create/flowable.mysql.create.history.sql"
);

private static final String FILE_NAME = "create.sql";

public static void createSqlFile(String targetPath) {
File targetSqlFile;
try {
targetSqlFile = getSqlFile(targetPath, FILE_NAME);
} catch (IOException e) {
throw new RuntimeException(e);
}

try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetSqlFile))) {

appendCreateDatabaseSql(outputStream);

for (String sqlPath : SQL_PATH_LIST) {
appendCreateTableSql(outputStream, sqlPath);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

static File getSqlFile(String targetPath, String fileName) throws IOException {
if (targetPath == null) {
throw new NullPointerException();
}

File targetDir = new File(targetPath);

if (!targetDir.exists()) {
throw new FileNotFoundException(targetPath + " is not exists");
}

File sqlFile = new File(targetDir.getAbsolutePath() + File.separator + fileName);

if (sqlFile.exists() && !sqlFile.delete()) {
throw new IOException("failed to delete file " + sqlFile.getAbsolutePath());
} else if (!sqlFile.createNewFile()) {
throw new IOException("failed to create file " + sqlFile.getAbsolutePath());
}

return sqlFile;
}

private static void appendCreateDatabaseSql(OutputStream outputStream) throws IOException {
outputStream.write(("/**************************************************************/\n" +
"/* [START CREATING DATABASE]\n" +
"/**************************************************************/\n").getBytes());

outputStream.write("DROP DATABASE IF EXISTS `flowable`;\n".getBytes());
outputStream.write("CREATE DATABASE `flowable`;\n".getBytes());
outputStream.write("USE `flowable`;\n".getBytes());

outputStream.write(("/**************************************************************/\n" +
"/* [END CREATING DATABASE]\n" +
"/**************************************************************/\n").getBytes());

outputStream.write("\n\n\n".getBytes());
}

static void appendCreateTableSql(OutputStream outputStream, String fileClassPath) throws IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

String simpleFilePath = fileClassPath.substring(fileClassPath.lastIndexOf(File.separator) + 1).trim();

InputStream inputStream = classLoader.getResourceAsStream(fileClassPath);

if (inputStream == null) {
System.err.println(fileClassPath);
return;
}

System.out.println(fileClassPath);

outputStream.write(("/**************************************************************/\n" +
"/* [START]\n" +
"/* " + simpleFilePath + "\n" +
"/**************************************************************/\n").getBytes());

IOUtils.copy(inputStream, outputStream);
outputStream.write("\n".getBytes());

outputStream.write(("/**************************************************************/\n" +
"/* [END]\n" +
"/* " + simpleFilePath + "\n" +
"/**************************************************************/\n").getBytes());

outputStream.write("\n\n\n".getBytes());
inputStream.close();
}

public static void main(String[] args) {
createSqlFile("/Users/hechenfeng/Desktop/flowable");
}

}

4.8 UpgradeSqlUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package org.liuyehcf.flowable.utils;

import org.apache.commons.io.IOUtils;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.liuyehcf.flowable.utils.CreateSqlUtils.appendCreateTableSql;
import static org.liuyehcf.flowable.utils.CreateSqlUtils.getSqlFile;

/**
* @author chenlu
* @date 2018/8/30
*/
public class UpgradeSqlUtils {

private static final String OLD_VERSION = "oldVersion";
private static final String NEW_VERSION = "newVersion";

private static final List<String> SQL_PATH_LIST = Arrays.asList(
"org/flowable/common/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.common.sql",

"org/flowable/idm/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.identity.sql",
"org/flowable/identitylink/service/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.identitylink.sql",

"org/flowable/variable/service/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.variable.sql",

"org/flowable/job/service/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.job.sql",

"org/flowable/task/service/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.task.sql",

"org/flowable/db/upgrade/flowable.all.upgradestep.${oldVersion}.to.${newVersion}.engine.sql",

"org/flowable/idm/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.identity.sql",
"org/flowable/identitylink/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.identitylink.sql",
"org/flowable/identitylink/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.identitylink.history.sql",

"org/flowable/variable/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.variable.sql",
"org/flowable/variable/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.variable.history.sql",

"org/flowable/job/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.job.sql",

"org/flowable/task/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.task.sql",
"org/flowable/task/service/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.task.history.sql",

"org/flowable/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.engine.sql",
"org/flowable/db/upgrade/flowable.mysql.upgradestep.${oldVersion}.to.${newVersion}.history.sql"
);

private static final String FILE_NAME = "update_${oldVersion}_to_${newVersion}.sql";

private static String createSqlFile(String targetPath, String oldVersion, String newVersion) {
File targetSqlFile;
try {
targetSqlFile = getSqlFile(targetPath, resolvePlaceHolder(FILE_NAME, oldVersion, newVersion));
} catch (IOException e) {
throw new RuntimeException(e);
}

try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetSqlFile))) {

for (String sqlPath : SQL_PATH_LIST) {

appendCreateTableSql(outputStream, resolvePlaceHolder(sqlPath, oldVersion, newVersion));
}
} catch (IOException e) {
throw new RuntimeException(e);
}

return targetSqlFile.getAbsolutePath();
}

private static String resolvePlaceHolder(String sqlPath, String oldVersion, String newVersion) {
String actualSqlPath = sqlPath.replace("${" + OLD_VERSION + "}", oldVersion);
actualSqlPath = actualSqlPath.replace("${" + NEW_VERSION + "}", newVersion);
return actualSqlPath;
}

public static void main(String[] args) throws Exception {
String targetDir = "/Users/hechenfeng/Desktop/flowable";

List<String> updateVersions = Arrays.asList("6120", "6200", "6210", "6300", "6301");
List<String> filePaths = new ArrayList<>();

int updateTimes = updateVersions.size() - 1;

/*
* 每次更新生成更新sql文件
*/
for (int i = 0; i < updateTimes; i++) {
String oldVersion = updateVersions.get(i);
String newVersion = updateVersions.get(i + 1);

System.out.println("\n\nfrom " + oldVersion + " to " + newVersion);

String sqlFile = createSqlFile(targetDir, oldVersion, newVersion);
filePaths.add(sqlFile);
}

/*
* 生成 merge 文件名
*/
StringBuilder sb = new StringBuilder();
sb.append(targetDir)
.append('/')
.append("update");

for (String updateVersion : updateVersions) {
sb.append('_')
.append(updateVersion);
}
sb.append(".sql");

/*
* merge 每次更新的sql,生成一个sql
*/
OutputStream out = new FileOutputStream(sb.toString());
for (int i = 0; i < updateTimes; i++) {
FileInputStream in = new FileInputStream(filePaths.get(i));
IOUtils.copy(in, out);
in.close();
}
out.close();
}

}

4.9 DemoController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.liuyehcf.flowable.web;

import org.liuyehcf.flowable.service.DemoService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
* @author hechenfeng
* @date 2018/7/25
*/
@RestController
@RequestMapping("/")
public class DemoController {

@Resource
private DemoService demoService;

@RequestMapping("/process/deploy")
@ResponseBody
public String deployProcess() {
String processDefinitionId = demoService.deployProcess();
return "Deploy Succeeded, processDefinitionId=" + processDefinitionId + "\n";
}

@RequestMapping("/process/start")
@ResponseBody
public String startProcess(@RequestParam String processDefinitionId) {
String processInstanceId = demoService.startProcess(processDefinitionId);
return "Start Succeeded, processInstance=" + processInstanceId + "\n";
}

@RequestMapping("/userTask/completeByAssignee")
@ResponseBody
public String completeUserTaskByAssignee(@RequestParam String assignee) {
return demoService.completeUserTaskByAssignee(assignee) + "\n";
}

@RequestMapping("/userTask/completeByCandidateUser")
@ResponseBody
public String completeUserTask(@RequestParam String candidateUser) {
return demoService.completeUserTaskByCandidateUser(candidateUser) + "\n";
}
}

5 Resource

5.1 application.properties

1
2
3
4
server.port=7001
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/flowable?autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=xxx

5.2 logback.xml

1
2
3
4
5
6
7
8
9
10
11
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

5.3 sample.bpmn20.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="http://www.flowable.org/processdef">

<process id="process1" name="process1" isExecutable="true">
<documentation>Example</documentation>

<startEvent id="startEvent1">
<extensionElements>
<flowable:executionListener event="start" delegateExpression="${demoListener}"/>
</extensionElements>
</startEvent>

<serviceTask id="serviceTask1" name="serviceTask1" flowable:async="true"
flowable:delegateExpression="${demoServiceTask}">
<extensionElements>
<flowable:field name="field1" stringValue="someValue1"/>

<flowable:field name="field2">
<flowable:string><![CDATA[someValue2]]></flowable:string>
</flowable:field>
<flowable:taskListener event="create" delegateExpression="${demoListener}"/>
</extensionElements>
</serviceTask>

<userTask id="userTask1" name="userTask1" flowable:assignee="tom">
<extensionElements>
<flowable:taskListener event="create" delegateExpression="${demoListener}"/>
</extensionElements>
</userTask>

<userTask id="userTask2" name="userTask2" flowable:candidateUsers="bob,jerry">
<extensionElements>
<flowable:taskListener event="create" delegateExpression="${demoListener}"/>
</extensionElements>
</userTask>

<userTask id="userTask3" name="userTask3" flowable:candidateUsers="jerry,lucy">
<extensionElements>
<flowable:taskListener event="create" delegateExpression="${demoListener}"/>
</extensionElements>
</userTask>

<endEvent id="endEvent1">
<extensionElements>
<flowable:executionListener event="start" delegateExpression="${demoListener}"/>
</extensionElements>
</endEvent>

<sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="serviceTask1"/>
<sequenceFlow id="flow2" sourceRef="serviceTask1" targetRef="userTask1"/>
<sequenceFlow id="flow3" sourceRef="userTask1" targetRef="userTask2"/>
<sequenceFlow id="flow4" sourceRef="userTask2" targetRef="userTask3"/>
<sequenceFlow id="flow5" sourceRef="userTask3" targetRef="endEvent1"/>
</process>
</definitions>

6 步骤

6.1 数据库初始化

详细步骤如下

  1. 首先执行CreateSqlUtils中的main函数,创建sql文件
  2. 执行命令mysql -u root -p登录数据库
  3. mysql会话中执行sql文件:source <yourDir>/create.sql
  • 至此,建库建表工作完成

6.2 部署工作流

部署工作流

启动工作流实例(将上面的processDefinitionId填入下方url中)

完成UserTask1

完成UserTask2

完成UserTask3

日志如下

1
2
3
4
5
6
7
8
9
10
74514 [http-nio-7001-exec-1] INFO  o.l.flowalbe.service.DemoService - Deploy process success! processDefinition=process1:1:3 
133034 [http-nio-7001-exec-2] INFO o.l.flowalbe.service.DemoService - Deploy process success! processDefinition=process1:2:6
270605 [http-nio-7001-exec-5] INFO o.l.flowalbe.element.DemoListener - ExecutionListener is trigger. elementId=startEvent1
270637 [http-nio-7001-exec-5] INFO o.l.flowalbe.service.DemoService - Start process success! processDefinitionId=process1:2:6; processInstanceId=7
270705 [SimpleAsyncTaskExecutor-1] INFO o.l.f.element.DemoServiceTask - Filed injection succeeded. fieldName=field1
270705 [SimpleAsyncTaskExecutor-1] ERROR o.l.f.element.DemoServiceTask - Filed injection failed. fieldName=field2
270792 [SimpleAsyncTaskExecutor-1] INFO o.l.flowalbe.element.DemoListener - TaskListener is trigger. taskName=userTask1; assignee=tom; candidateInfoList=[]
322831 [http-nio-7001-exec-7] INFO o.l.flowalbe.element.DemoListener - TaskListener is trigger. taskName=userTask2; assignee=null; candidateInfoList=[{"userId":"jerry"},{"userId":"bob"}]
473009 [http-nio-7001-exec-10] INFO o.l.flowalbe.element.DemoListener - TaskListener is trigger. taskName=userTask3; assignee=null; candidateInfoList=[{"userId":"jerry"},{"userId":"lucy"}]
508793 [http-nio-7001-exec-2] INFO o.l.flowalbe.element.DemoListener - ExecutionListener is trigger. elementId=endEvent1

7 Test

7.1 DemoTest

@ContextHierarchy将创建子容器,EmbeddedDatabaseConfig将会在子容器中加载,会覆盖父容器的同名Bean,通过这种方式来替换数据源的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package org.liuyehcf.flowable.test;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.liuyehcf.flowable.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.concurrent.TimeUnit;

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {TestApplication.class})
public class DemoTest {

@Autowired
private DemoService demoService;

@Test
public void test() {
String processDefinition = demoService.deployProcess();
log.info("deployProcess succeeded. processDefinition={}", processDefinition);

String processInstanceId = demoService.startProcess(processDefinition);
log.info("startProcess succeeded. processInstanceId={}", processInstanceId);

sleep(1);
String message;

message = demoService.completeUserTaskByAssignee("tom");
log.info("completeUserTaskByAssignee. message={}", message);

sleep(1);

message = demoService.completeUserTaskByCandidateUser("bob");
log.info("completeUserTaskByCandidateUser. message={}", message);

sleep(1);

message = demoService.completeUserTaskByCandidateUser("lucy");
log.info("completeUserTaskByCandidateUser. message={}", message);
}

private static void sleep(int second) {
try {
TimeUnit.SECONDS.sleep(second);
} catch (InterruptedException e) {
// ignore
}
}
}

7.2 EmbeddedDatabaseConfig

配置了H2 database,即内存数据库来进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.liuyehcf.flowable.test;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

import javax.sql.DataSource;

@Configuration
public class EmbeddedDatabaseConfig {

@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}

@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}

}

7.3 TestApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.liuyehcf.flowable.test;

import org.liuyehcf.flowable.Application;
import org.liuyehcf.flowable.config.DataSourceConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication
@ComponentScan(
basePackages = "org.liuyehcf.flowable",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {Application.class, DataSourceConfig.class}
)
)
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}

7.4 logback-test.xml

Test中的logback的配置文件必须为logback-test.xml才能生效

1
2
3
4
5
6
7
8
9
10
11
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

8 那些年我们一起踩过的坑

8.1 ServiceTask Field Injection

从上面的日志中,我们可以看到DemoServiceTaskfield2注入失败,而field1field2的唯一区别在于field1publicsetter方法

情景还原

  1. 一个JavaDelegateorg.flowable.engine.delegate.JavaDelegate)的实现类DemoServiceTask,该实现类包含了一个类型为Expression(org.flowable.engine.common.api.delegate.Expression)的字段field2,且该字段没有publicsetter方法
  2. 一个Spring AOP,拦截了该DemoServiceTask,AOP配置源码已经在之前的小节中给出

给定的BPMN文件也很简单,直接在xml文件中写入了field2的数值,BPMN文件源码在之前的小节已经给出

当运行后,执行到DemoServiceTask.execute方法时,field2null。这就比较奇怪了,所有的配置看起来都没有问题。后来查阅Flowable官方文档,上面说,字段注入如果不提供publicsetter方法,而仅仅只是提供private字段,可能会有Security的问题

为了验证上面的说法,本机DEBUG,验证的起点是org.flowable.engine.impl.bpmn.behavior.ServiceTaskDelegateExpressionActivityBehavior这个类的execute方法。最终调用到了org.flowable.engine.common.impl.util.ReflectUtilinvokeSetterOrField方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void invokeSetterOrField(Object target, String name, Object value, boolean throwExceptionOnMissingField) {
Method setterMethod = getSetter(name, target.getClass(), value.getClass());

if (setterMethod != null) {
invokeSetter(setterMethod, target, name, value);

} else {
Field field = ReflectUtil.getField(name, target);
if (field == null) {
if (throwExceptionOnMissingField) {
throw new FlowableIllegalArgumentException("Field definition uses unexisting field '" + name + "' on class " + target.getClass().getName());
} else {
return;
}
}

// Check if the delegate field's type is correct
if (!fieldTypeCompatible(value, field)) {
throw new FlowableIllegalArgumentException("Incompatible type set on field declaration '" + name
+ "' for class " + target.getClass().getName()
+ ". Declared value has type " + value.getClass().getName()
+ ", while expecting " + field.getType().getName());
}

setField(field, target, value);
}
}

public static void setField(Field field, Object object, Object value) {
try {
field.setAccessible(true);
field.set(object, value);
} catch (IllegalArgumentException e) {
throw new FlowableException("Could not set field " + field.toString(), e);
} catch (IllegalAccessException e) {
throw new FlowableException("Could not set field " + field.toString(), e);
}
}

上述setField成功调用,说明field2字段设置成功了。重点来了,这里的object(setField方法的第二个参数),也就是目标对象,并不是DemoServiceTask的实例,而是一个Cglib的代理类,这个代理类同样包含了一个field2字段,因此setField仅仅设置了Cglib的代理类的field2字段而已。当执行到目标方法,也就是DemoServiceTask类的execute方法中时,我们取的是DemoServiceTaskfield2字段,也就是说,DemoServiceTask根本无法取到那个设置到Cglib的代理类中去的field2字段

其实,这并不是什么Security造成的问题,而是AOP使用时的细节问题

8.2 Test

在测试方法中不要加@Transactional注解,由于工作流的执行是由工作流引擎完成的,并不是在当前测试方法中完成的,因此在别的线程无法拿到Test方法所在线程尚未提交的数据

8.3 xml文件名

1
2
3
4
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("async-service.xml")
.deploy();

文件名为async-service.xml时部署失败,改成async-service.bpmn20.xml后部署成功

8.4 Table “ACT_RU_JOB” not found

情景还原:配置了两个异步的ServiceTask,执行部署、启动的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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: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"
xmlns:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">

<process id="processId" name="testQueryCandidateInfoListener" isExecutable="true">
<documentation>Example</documentation>

<startEvent id="startEvent"/>

<serviceTask id="serviceTask1" name="serviceTask1" flowable:delegateExpression="${printTimeService}"
flowable:async="true"/>

<serviceTask id="serviceTask2" name="serviceTask1" flowable:delegateExpression="${printTimeService}"
flowable:async="true">
<extensionElements>
<flowable:field name="seconds" stringValue="10"/>
</extensionElements>
</serviceTask>

<userTask id="userTask" name="userTask" flowable:assignee="assignee"/>

<endEvent id="endEvent"/>

<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="serviceTask1"/>
<sequenceFlow id="flow2" sourceRef="serviceTask1" targetRef="serviceTask2"/>
<sequenceFlow id="flow3" sourceRef="serviceTask2" targetRef="userTask"/>
<sequenceFlow id="flow4" sourceRef="userTask" targetRef="endEvent"/>
</process>
</definitions>

单元测试运行过程中,出现了如下的错误

1
2
3
4
5
### The error may exist in org/flowable/job/service/db/mapping/entity/Job.xml
### The error may involve org.flowable.job.service.impl.persistence.entity.JobEntityImpl.selectJob
### The error occurred while executing a query
### SQL: select * from ACT_RU_JOB where ID_ = ?
### Cause: org.h2.jdbc.JdbcSQLException: Table "ACT_RU_JOB" not found; SQL statement:

原因分析:由于ServiceTask配置的是异步方式,因此Test执行线程结束后,整个进程就被终止了,因此导致的这个问题。解决方法:Test最后sleep一段时间即可