1. 版本问题
1.1. Activiti版本
7.1.0-M6是最后一个支持JDK1.8的版本,此后的版本都要求JDK11以上
目前,Activiti最新版本是7.6.0,它是用JDK11编译的,因此要想使用最新版7.6.0必须升级JDK版本,不能再用1.8
同时,7.6.0依赖的SpringBoot版本是2.7.5
1.2. SpringBoot版本
最新的SpringBoot版本是3.0.0,这个版本不支持JDK1.8,对JDK的最小版本是17
目前可用比较多的Java版本是Java 17 和 Java 19
综合来看,我们采用Java 17+SpringBoot 2.7.5+Activiti 7.6.1
补充:52 = Java 8 55 = Java 11
2. Maven仓库设置
首先要添加Activiti组件的仓库,不然找不到jar包,可用配置在全局的settings.xml文件中,也可以配置在项目pom.xml中
<repositories>
<repository>
<id>activiti-releases</id>
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
</repository>
</repositories>
如果settings.xml中有镜像,并且所有镜像都使用一个仓库的话(即,morrorOf配置的是*),要注意将新加的这个仓库排除
Maven – Guide to Mirror Settings
举个例子:
3. 依赖管理
Activiti依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.6.1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
Dubbo依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>3.1.3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
完整的父POM如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>3.0.0</version>-->
<version>2.7.5</version>
<relativePath/>
</parent>
<groupId>com.cjs.example</groupId>
<artifactId>activiti7-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>activiti7-sample</name>
<modules>
<module>activiti7-sample-provider-api</module>
<module>activiti7-sample-provider</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M6</version>
</dependency>
-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.6.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>3.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>activiti-releases</id>
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
</repository>
</repositories>
</project>
4. Activiti API使用
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cjs.example</groupId>
<artifactId>activiti7-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.cjs.example</groupId>
<artifactId>activiti7-sample-provider</artifactId>
<version>${parent.version}</version>
<name>activiti7-sample-provider</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.cjs.example</groupId>
<artifactId>activiti7-sample-provider-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8080
servlet:
context-path: /activiti7
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/activiti7?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&nullCatalogMeansCurrent=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
activiti:
check-process-definitions: false
database-schema-update: "false" # 第一次运行时设置为true,待表生成完以后改成false,以后就不用再更新表结构了
db-history-used: true
history-level: full
deployment-mode: "never-fail" # org.activiti.spring.autodeployment.AbstractAutoDeploymentStrategy
dubbo:
application:
name: activiti7-sample-provider
protocol:
name: dubbo
port: -1
registry:
address: nacos://127.0.0.1:8848
config-center:
address: nacos://127.0.0.1:8848
metadata-report:
address: nacos://127.0.0.1:8848
启动项目后,生成的表结构如图:
也可以自己执行SQL脚本,脚本在源码包activiti-engine-7.6.1.jar里面
由于Activit7集成了SpringSecurity,它用SpringSecurity来做权限管理,因此它需要一个UserDetailsService,不配做的话启动会报错。当然网上也有一种解决办法就是排除SpringSecurity相关的某些类,没试过,应该也是可以的吧。
按照Spring Security 5.7以后的新写法,我们来配置一下WebSecurity
Java Configuration :: Spring Security
https://github.com/spring-projects/spring-security-samples/tree/6.0.x/servlet/spring-boot/java/oauth2
package com.cjs.example.provider.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
/**
* https://docs.spring.io/spring-security/reference/servlet/configuration/java.html
* https://github.com/spring-projects/spring-security-samples/tree/6.0.x/servlet/spring-boot/java/oauth2
*
* @Author: ChengJianSheng
* @Date: 2022/12/5
*/
@Configuration
@EnableWebSecurity
public class MyWebSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
// Spring Security should completely ignore URLs starting with /resources/
.antMatchers("/resources/**");
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withUsername("tom")
.password("$2a$10$tk29HhXGXCZDOSvn.VZlFeBsLmtJrKE2Uv6zLrRpTyvZMu3ipQLgC")
.roles("ACTIVITI_USER", "ACTIVITI_ADMIN", "APPLICATION_MANAGER")
.build();
UserDetails user = User.withUsername("jerry")
.password("$2a$10$tk29HhXGXCZDOSvn.VZlFeBsLmtJrKE2Uv6zLrRpTyvZMu3ipQLgC")
.roles("ACTIVITI_USER", "GROUP_BUSINESS_MANAGER")
.build();
UserDetails zhangsan = User.withUsername("zhangsan")
.password("$2a$10$tk29HhXGXCZDOSvn.VZlFeBsLmtJrKE2Uv6zLrRpTyvZMu3ipQLgC")
.roles("ACTIVITI_USER")
.build();
UserDetails lisi = User.withUsername("lisi")
.password("$2a$10$tk29HhXGXCZDOSvn.VZlFeBsLmtJrKE2Uv6zLrRpTyvZMu3ipQLgC")
.roles("ACTIVITI_USER")
.build();
return new InMemoryUserDetailsManager(admin, user, zhangsan, lisi);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123456"));
}
}
在activiti-spring-boot-starter中新提供了ProcessRuntime 和 TaskRuntime用于流程和任务处理的API,调用它们需要当前操作的用户具有ACTIVITI_USER权限
因此,在使用这两个类之前要保证当前登录用户有这个权限。为了模拟登录,我们来写个登录方法。
package com.cjs.example.provider.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @Author: ChengJianSheng
* @Date: 2022/12/6
*/
@Component
public class SecurityUtil {
@Autowired
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (null == user) {
throw new IllegalStateException(String.format("用户【%s】不存在", username));
}
SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
}
现在可以开始调用API操作流程了
package com.cjs.example.provider.service;
import com.cjs.example.provider.api.WorkflowService;
import com.cjs.example.provider.util.SecurityUtil;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.model.payloads.GetProcessInstancesPayload;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Order;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.repository.Deployment;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
/**
* @Author: ChengJianSheng
* @Date: 2022/12/6
*/
@DubboService
public class WorkflowServiceImpl implements WorkflowService {
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private TaskRuntime taskRuntime;
@Autowired
private RuntimeService runtimeService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private SecurityUtil securityUtil;
@Override
public void deploy() {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("processes/leave.bpmn20.xml")
.name("请假")
.key("leave")
.category("AAA")
.tenantId("QingJia")
.deploy();
}
@Override
public void start() {
securityUtil.logInAs("tom");
ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder.start()
.withProcessDefinitionKey("leave")
.withVariable("hello", "world")
.withVariable("apple", "orange")
.withBusinessKey("1")
.build());
}
@Override
public void startProcessInstance(String processDefinitionKey, String businessKey, Map<String, Object> variables, String tenantId) {
runtimeService.startProcessInstanceByKeyAndTenantId(processDefinitionKey, businessKey, variables, tenantId);
}
@Override
public void processInstancePage(int pageNo, int pageSize) {
securityUtil.logInAs("tom");
GetProcessInstancesPayload payload = new GetProcessInstancesPayload();
payload.setActiveOnly(true);
Page<ProcessInstance> page = processRuntime.processInstances(Pageable.of(pageNo-1, pageSize, Order.by("start_time_", Order.Direction.DESC)), payload);
for (ProcessInstance ps : page.getContent()) {
System.out.println(ps);
}
}
@Override
public void taskList() {
securityUtil.logInAs("zhangsan");
Page<Task> taskPage = taskRuntime.tasks(Pageable.of(0, 10));
System.out.println(taskPage.getTotalItems());
securityUtil.logInAs("lisi");
taskPage = taskRuntime.tasks(Pageable.of(0, 10));
System.out.println(taskPage.getTotalItems());
}
@Override
public void claimTask() {
securityUtil.logInAs("zhangsan");
String taskId = "429ad159-754c-11ed-aaf8-84a9386654d8";
Task task = taskRuntime.task(taskId);
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
}
@Override
public void completeTask() {
securityUtil.logInAs("zhangsan");
String taskId = "429ad159-754c-11ed-aaf8-84a9386654d8";
taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
}
}
注意:可以把tenantId看成是某个业务,businessKey当做业务ID,比如:放款流程001。或者建一张中间表用于关联流程ID和业务ID。
最后,配置流程监听器
package com.cjs.example.provider.config;
import lombok.extern.slf4j.Slf4j;
import org.activiti.api.process.runtime.events.ProcessCompletedEvent;
import org.activiti.api.process.runtime.events.listener.ProcessRuntimeEventListener;
import org.activiti.api.task.runtime.events.TaskAssignedEvent;
import org.activiti.api.task.runtime.events.TaskCompletedEvent;
import org.activiti.api.task.runtime.events.listener.TaskRuntimeEventListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: ChengJianSheng
* @Date: 2022/12/6
*/
@Slf4j
@Configuration
public class ListenerConfig {
@Bean
public ProcessRuntimeEventListener<ProcessCompletedEvent> processCompletedListener() {
return processCompleted -> log.info(">>> Process Completed: '"
+ processCompleted.getEntity().getName() +
"' We can send a notification to the initiator: " + processCompleted.getEntity().getInitiator());
}
@Bean
public TaskRuntimeEventListener<TaskAssignedEvent> taskAssignedListener() {
return taskAssigned -> log.info(">>> Task Assigned: '"
+ taskAssigned.getEntity().getName() +
"' We can send a notification to the assginee: " + taskAssigned.getEntity().getAssignee());
}
@Bean
public TaskRuntimeEventListener<TaskCompletedEvent> taskCompletedListener() {
return taskCompleted -> log.info(">>> Task Completed: '"
+ taskCompleted.getEntity().getName() +
"' We can send a notification to the owner: " + taskCompleted.getEntity().getOwner());
}
}