Dynamic Multi‑Approver Assignment in Activiti: Using Candidate Users with Spring Boot

This guide demonstrates how to configure Activiti 7 with Spring Boot to assign multiple candidate approvers for a task, implement a service that supplies candidate user IDs, expose controller endpoints to start and complete the process, and troubleshoot common security‑related errors.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Dynamic Multi‑Approver Assignment in Activiti: Using Candidate Users with Spring Boot

Process Design

The workflow runs on Spring Boot 2.3.12.RELEASE with Activiti 7.1.0.M6. Instead of a single assignee, the process uses a candidate‑users expression so that any of several users can complete the task.

<?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:activiti="http://activiti.org/bpmn" 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" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.xg.com">
  <process id="cand" name="cand" isExecutable="true">
    <startEvent id="startevent1" name="Start"/>
    <endEvent id="endevent1" name="End"/>
    <userTask id="empApprove" name="出差申请" activiti:assignee="${empId}"/>
    <userTask id="usertask2" name="部门经理" activiti:candidateUsers="${userService.deptEmps(\"1\")}"/>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="empApprove"/>
    <sequenceFlow id="flow2" sourceRef="empApprove" targetRef="usertask2"/>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="endevent1"/>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_cand">
    <bpmndi:BPMNPlane bpmnElement="cand" id="BPMNPlane_cand">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="470.0" y="80.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="470.0" y="480.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="empApprove" id="BPMNShape_empApprove">
        <omgdc:Bounds height="55.0" width="105.0" x="435.0" y="190.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="435.0" y="310.0"/>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Service Component

@Service
public class UserService {
    public String deptEmps(String deptId) {
        StringJoiner emps = new StringJoiner(",");
        if ("1".equals(deptId)) {
            emps.add("30000");
            emps.add("30001");
        } else if ("2".equals(deptId)) {
            emps.add("40000");
            emps.add("40001");
        }
        return emps.toString();
    }
}

Controller Endpoint

@GetMapping("/startCand")
public R startProcessCand(String processDefinitionId, String deptId) {
    Map<String, Object> variables = new HashMap<>();
    variables.put("empId", "00000");
    ProcessInstance instance = ls.startProcessInstanceAssignVariables(processDefinitionId, variables);
    // Pass the UserService bean as a variable so the candidate expression can call it
    variables.put("userService", userService);
    ls.executionTask(variables, instance.getId());
    return R.success();
}

Start Process Visuals

At this point the ASSIGNEE_ variable is empty, and the runtime task table shows two candidate users (30000 and 30001) generated for the department‑manager task.

Security Considerations

Activiti uses Spring Security’s UserDetailsService to load users. If a candidate user (e.g., 30002) does not exist, a 403 error occurs.

public class ActivitiUserGroupManagerImpl implements UserGroupManager {
    private final UserDetailsService userDetailsService;
    public ActivitiUserGroupManagerImpl(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    @Override
    public List<String> getUserGroups(String username) {
        return userDetailsService.loadUserByUsername(username).getAuthorities().stream()
            .filter(a -> a.getAuthority().startsWith("GROUP_"))
            .map(a -> a.getAuthority().substring(6))
            .collect(Collectors.toList());
    }
}

To avoid the error, either provide a custom UserDetailsService that knows the candidate users, or replace the default UserGroupManager bean with your own implementation.

View Process Diagram

Approve Endpoint

@PostMapping("/approve")
public R approve(@RequestBody Map<String, Object> variables) {
    String instanceId = (String) variables.remove("instanceId");
    if (StringUtils.isEmpty(instanceId)) {
        return R.failure("未知任务");
    }
    ls.executionTask(variables, instanceId);
    return R.success();
}

Only the instanceId is required; other variables are optional.

Process Completion

The runtime task table now shows that the process has finished successfully.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootCandidate Users
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.