本文记录所有踩坑经历与解决方案,附带完整代码,希望能帮你节省时间。

 一、背景

工作流引擎不是新东西,但很多初学者在第一步——**整合 Spring Boot**——就会被各种依赖冲突、自动建表失败、测试用例跑不通等问题劝退。本文从零开始,搭建一个基于 Spring Boot 2.7.18 + Activiti 7.1.0.M6 的请假审批 Demo,包含部署、启动、员工申请、经理审批、HR 备案、驳回等完整功能。

 二、大坑预警:Activiti 与 MyBatis-Plus 的版本冲突

**这是整个整合过程中最恶心的问题,没有之一。**

Activiti 7.1.0.M6 内部依赖了 **MyBatis 3.5.0**。而 MyBatis-Plus 3.5.x 需要 MyBatis 3.5.9+,因为新版本中 `Configuration` 类增加了 `setShrinkWhitespacesInSql(boolean)` 和 `parsePendingMethods(boolean)` 等方法。如果你的项目同时引入了 Activiti 和 MyBatis-Plus,启动时会报类似:

```
java.lang.NoSuchMethodError: 'void org.apache.ibatis.session.Configuration.setShrinkWhitespacesInSql(boolean)'
```

 尝试过的解决方案(均失败)

1. 在 pom.xml 中排除 Activiti 传递的 mybatis,并手动指定高版本
           xml
           <exclusion>
               <groupId>org.mybatis</groupId>
               <artifactId>mybatis</artifactId>
           </exclusion>
   <!-- 再手动引入 3.5.13 -->
  依然报错,因为 IDEA / Maven 的依赖仲裁有时会“顽强”地拉低版本。

2. dependencyManagement 强制锁定版本

        仍然无效,运行时 classpath 中还是出现 3.5.0。

 最终出路一:彻底放弃 MyBatis-Plus

改用 Spring Data JPA。业务表 `leave_order` 只有简单增删改查,JPA 完全够用,而且与 Activiti 零冲突。
 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

最终出路二(经验证可行):升级 MyBatis 版本

如果坚持使用 MyBatis-Plus,可以尝试将 MyBatis 版本升级到 **3.5.16**(配合 MyBatis-Plus 3.5.7),经实际测试可以解决冲突。pom 配置如下

<properties>
    <mybatis.version>3.5.16</mybatis.version>
    <mybatis-plus.version>3.5.7</mybatis-plus.version>
</properties>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

并在 Activiti 依赖中排除旧 mybatis:
 

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
    </exclusions>
</dependency>

> 注意:如果排除后仍然报错,请检查是否还有其他依赖(如 `activiti-engine`)传递了旧版本,需要一并排除。

 三、第二个坑:Activiti 自动建表失败

配置明明写了 `spring.activiti.database-schema-update=true`,但启动后数据库里一张 `ACT_` 开头的表都没有。

原因分析

1. `application.yml` 中漏写了数据源配置(很多人把数据源写在 `application.properties` 里,但忘记移到 `yml` 中,而 Activiti 只认当前文件中的配置)。


2. MySQL 8.x 需要额外参数:`nullCatalogMeansCurrent=true`,否则 Activiti 会扫描所有库而找不到表。


3. 数据库用户权限不足:无法执行 `CREATE TABLE`。

解决方案

确保 `application.yml` 中同时包含数据源和 Activiti 配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/activiti_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  activiti:
    database-schema-update: true
    db-history-used: true
    history-level: full
    async-executor-activate: false
  security:
    enabled: false   # 禁用 Activiti 自带的 Spring Security,否则会要求 UserDetailsService

重启后,控制台会看到类似日志:
performing create on engine with resource org/activiti/db/create/activiti.mysql.create.engine.sql

然后数据库中出现 25 张 `ACT_*` 表。(后来我才发现,我的数据库没连上,导致没有及时刷新而对应的所有表都没有,后来一刷新全出来了)

---

 四、第三个坑:如果你只是单纯的想使用工作流那就得注意他自带的Security里面的坑太多了


    在整合 Activiti 工作流引擎时,很多开发者会直接引入 activiti-spring-boot-starter,然后惊讶地发现:项目还没开始写流程,启动就报错了,或者莫名其妙出现一个登录页面。这一切的根源,都是 Activiti 自带的 Spring Security 模块在作祟。

坑点1:启动失败,要求 UserDetailsService

NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.security.core.userdetails.UserDetailsService' available

原因:ActivitiSpringIdentityAutoConfiguration 中有一个 userGroupManager 方法依赖了 UserDetailsService,而你的项目没有提供。

坑点2:还是要求 UserGroupManager

解决方案

主要是这个usergroupManager你还不能不给他,activiti的配置里面没有明确的security配置让你去关闭,所以我们只能自己实现他的配置类,我的做法是这样的

package com.lucky.acitivitistudy.config;

import org.activiti.api.runtime.shared.identity.UserGroupManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Collections;
import java.util.List;

/**
 * Activiti身份管理配置,全部返回空列表,确保不受权限控制影响
 */
@Configuration
public class ActivitiIdentityConfig {

    @Bean
    public UserGroupManager userGroupManager() {
        return new UserGroupManager() {
            @Override
            public List<String> getUserGroups(String userId) {
                return Collections.emptyList();
            }

            @Override
            public List<String> getUserRoles(String s) {
                return List.of();
            }

            @Override
            public List<String> getGroups() {
                return List.of();
            }

            @Override
            public List<String> getUsers() {
                return List.of();
            }

        };
    }
}

直接返回空列表

然后在启动类上面加上这个:exclude排除一部分,就可以了

package com.lucky.acitivitistudy;

import org.activiti.api.runtime.shared.identity.UserGroupManager;
import org.activiti.core.common.spring.identity.config.ActivitiSpringIdentityAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication(exclude = {
        SecurityAutoConfiguration.class,
        ActivitiSpringIdentityAutoConfiguration.class
})
public class AcitivitiStudyApplication {

    public static void main(String[] args) {
        SpringApplication.run(AcitivitiStudyApplication.class, args);
    }

}

五、下面是我的代码和测试(需要的可以去github上自行拉取代码)

链接:Lucky

测试用例:

package com.lucky.acitivitistudy;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AcitivitiStudyApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    private static String managerTaskId;   // 经理的任务ID

    @Test
    @Order(1)
    void testDeployProcess() throws Exception {
        mockMvc.perform(post("/api/leave/deploy"))
                .andExpect(status().isOk())
                .andExpect(content().string(org.hamcrest.Matchers.containsString("流程部署成功")))
                .andDo(result -> System.out.println("✅ 流程部署成功"));
    }

    @Test
    @Order(2)
    void testStartProcess() throws Exception {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("applicant", "zhangsan");
        params.add("days", "2");
        params.add("reason", "test");

        mockMvc.perform(post("/api/leave/start")
                        .params(params)
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().isOk())
                .andExpect(content().string(org.hamcrest.Matchers.containsString("请假申请已提交")))
                .andDo(result -> System.out.println("✅ 请假流程启动成功: " + result.getResponse().getContentAsString()));
    }

    @Test
    @Order(3)
    void testCompleteApplyTask() throws Exception {
        // 查询申请人 zhangsan 的待办任务(员工填写申请)
        MvcResult result = mockMvc.perform(get("/api/leave/tasks/todo")
                        .param("assignee", "zhangsan"))
                .andExpect(status().isOk())
                .andReturn();

        String json = result.getResponse().getContentAsString();
        JsonNode tasks = objectMapper.readTree(json);
        Assertions.assertTrue(tasks.isArray() && tasks.size() > 0, "未查询到申请人的待办任务");
        // 修改点:字段名为 "id"
        String applyTaskId = tasks.get(0).get("id").asText();

        // 完成员工申请任务
        mockMvc.perform(post("/api/leave/complete")
                        .param("taskId", applyTaskId)
                        .param("approved", "true")
                        .param("comment", "提交申请"))
                .andExpect(status().isOk())
                .andDo(result2 -> System.out.println("✅ 员工申请任务已完成"));
    }

    @Test
    @Order(4)
    void testQueryManagerTasks() throws Exception {
        // 现在流程应该流转到经理审批,查询经理的待办
        MvcResult result = mockMvc.perform(get("/api/leave/tasks/todo")
                        .param("assignee", "manager"))
                .andExpect(status().isOk())
                .andReturn();

        String json = result.getResponse().getContentAsString();
        JsonNode tasks = objectMapper.readTree(json);
        Assertions.assertTrue(tasks.isArray() && tasks.size() > 0, "未查询到经理的待办任务");
        // 修改点:字段名为 "id"
        managerTaskId = tasks.get(0).get("id").asText();
        System.out.println("✅ 查询到经理的待办任务,taskId=" + managerTaskId);
    }

    @Test
    @Order(5)
    void testCompleteHrTask() throws Exception {
        // 查询 hr 的待办任务
        MvcResult result = mockMvc.perform(get("/api/leave/tasks/todo")
                        .param("assignee", "hr"))
                .andExpect(status().isOk())
                .andReturn();
        String json = result.getResponse().getContentAsString();
        JsonNode tasks = objectMapper.readTree(json);
        Assertions.assertTrue(tasks.isArray() && tasks.size() > 0, "未查询到HR的待办任务");
        String hrTaskId = tasks.get(0).get("id").asText();

        mockMvc.perform(post("/api/leave/complete")
                        .param("taskId", hrTaskId)
                        .param("approved", "true")
                        .param("comment", "同意"))
                .andExpect(status().isOk());
        System.out.println("✅ HR任务已完成,流程结束");
    }

    @Test
    @Order(6)
    void testCompleteManagerTask() throws Exception {
        Assertions.assertNotNull(managerTaskId, "managerTaskId不应为空");

        mockMvc.perform(post("/api/leave/complete")
                        .param("taskId", managerTaskId)
                        .param("approved", "true")
                        .param("comment", "我同意了"))
                .andExpect(status().isOk())
                .andExpect(content().string(Matchers.containsString("任务处理完成")))
                .andDo(result -> System.out.println("✅ 经理审批完成: " + result.getResponse().getContentAsString()));
    }
}

如果小于等于三天的请假则执行order为1235,否则执行12345。

整体流程是这样的

员工 ──(填写申请)──> 完成申请任务 ──> 网关判断天数
                                                                               │
                    ┌──────────────────────┴────────────────┐
                    │ (days >= 3)                                                                                 │ (days < 3)
                    ▼                                             ▼
                经理审批 ──(同意)──> HR备案 ──> 结束            HR备案 ──> 结束
                    │
                    └──(驳回)──> 返回员工重新填写

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐