阅读更多
1 环境
IDEA
Maven3.5.3
Spring-Boot-2.0.4.RELEASE
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文件
主要依赖项如下
flowable
spring-boot
jdbc
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 > <dependency > <groupId > org.flowable</groupId > <artifactId > flowable-spring-boot-starter-basic</artifactId > <version > 6.3.0</version > </dependency > <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 > <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 > <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 > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.2.3</version > </dependency > <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;@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;@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;@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;@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;@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;@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;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;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 ; 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); } StringBuilder sb = new StringBuilder (); sb.append(targetDir) .append('/' ) .append("update" ); for (String updateVersion : updateVersions) { sb.append('_' ) .append(updateVersion); } sb.append(".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;@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 数据库初始化
详细步骤如下
首先执行CreateSqlUtils
中的main
函数,创建sql文件
执行命令mysql -u root -p
登录数据库
在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) { } } }
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
从上面的日志中,我们可以看到DemoServiceTask
的field2
注入失败,而field1
与field2
的唯一区别在于field1
有public
的setter
方法
情景还原 :
一个JavaDelegate
(org.flowable.engine.delegate.JavaDelegate
)的实现类DemoServiceTask
,该实现类包含了一个类型为Expression(org.flowable.engine.common.api.delegate.Expression)
的字段field2
,且该字段没有public
的setter
方法
一个Spring AOP,拦截了该DemoServiceTask
,AOP配置源码已经在之前的小节中给出
给定的BPMN
文件也很简单,直接在xml文件中写入了field2
的数值,BPMN
文件源码在之前的小节已经给出
当运行后,执行到DemoServiceTask.execute
方法时,field2
是null
。这就比较奇怪了,所有的配置看起来都没有问题。后来查阅Flowable官方文档 ,上面说,字段注入如果不提供public
的setter
方法,而仅仅只是提供private
字段,可能会有Security
的问题
为了验证上面的说法,本机DEBUG,验证的起点是org.flowable.engine.impl.bpmn.behavior.ServiceTaskDelegateExpressionActivityBehavior
这个类的execute
方法。最终调用到了org.flowable.engine.common.impl.util.ReflectUtil
的invokeSetterOrField
方法中
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 ; } } 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
方法中时,我们取的是DemoServiceTask
的field2
字段,也就是说,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一段时间即可