Activiti – Integrate SpringBoot

1. Introduce dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <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-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.activiti</groupId>
        <artifactId>activiti-spring-boot-starter</artifactId>
        <version>7.0.0.Beta2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.29</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

2. SpringBoot application.yml file configuration

Note: activiti7 does not enable database history by default and needs to be manually configured to enable it.

spring:
  datasource:
    url: jdbc:mysql:///activiti7?useUnicode=true & characterEncoding=utf8 & serverTimezone=GMT
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  Activiti:
    #1.flase: default value. When activiti starts, it compares the versions saved in the database tables. If there is no table or the versions do not match, an exception will be thrown.
    #2.true: activiti will update all tables in the database. If the table does not exist, it is automatically created
    #3.create_drop: Create the table when activiti starts and delete the table when it closes (the engine must be shut down manually to delete the table)
    #4.drop-create: Delete the original old table when activiti starts, and then create a new table (no need to shut down the engine manually)
    database-schema-update: true
    #Detect whether the history table exists. Activiti7 does not enable database history by default. Start database history.
    db-history-used: true
    #Record history level Configurable history levels include none, activity, audit, full
    #none: No historical data is saved, so this is the most efficient during process execution.
    #activity: The level is higher than none, the process instance and process behavior are saved, and other data is not saved.
    #audit: In addition to the data saved at the activity level, all process tasks and their attributes will also be saved. audit is the default value of history.
    #full: The highest level for saving historical data. In addition to saving audit-level data, it will also save all other process-related detailed data, including some process parameters, etc.
    history-level: full
    #Verify process files. By default, the process files in the processes folder under resources are verified.
    check-process-definitions: false

3. Startup class

@SpringBootApplication
public class AppApplication {<!-- -->
    public static void main(String[] args) {<!-- -->
        SpringApplication.run(AppApplication.class, args);
    }
}

4. Add SpringSecurity security framework integration configuration

Because after Activiti7 is integrated with SpringBoot, the SpringSecurity security framework is integrated by default, so we need to prepare the relevant user permission configuration information integrated by SpringSecurity.

SpringBoot’s dependency package has also added SpringSecurity’s dependency package to the project.

4.1, ### Add SecurityUtil class

A component added in order to quickly implement the configuration of the SpringSecurity security framework.

@Component
public class SecurityUtil {<!-- -->
    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    @Autowired
    @Qualifier("myUserDetailsService")
    private UserDetailsService userDetailsService;


    public void logInAs(String username) {<!-- -->
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {<!-- -->
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + 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 b) throws IllegalArgumentException {<!-- -->
                                
                            }

                            @Override
                            public String getName() {<!-- -->
                                return user.getUsername();
                            }
                        }
                )
        );
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}

4.2, ### Add DemoApplicationConfig class

Find the DemoApplicationConfig class in the officially downloaded Example of Activiti7. Its function is to implement the configuration of user permissions of the SpringSecurity framework, so that we can use user permission information in the system.

In this project, it is basically the user information defined in the file. Of course, it can also be the user permission information queried in the database.

The task leader who will be used later in the process needs to be added here.

@Configuration
public class ActivitiConfiguration {<!-- -->
    private Logger logger = LoggerFactory.getLogger(ActivitiConfiguration.class);

    @Bean
    public UserDetailsService myUserDetailsService() {<!-- -->
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        //Add the user here. The task leader who will be used later when processing the process needs to be added here.
        String[][] usersGroupsAndRoles = {<!-- -->
                {<!-- -->"jack", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {<!-- -->"rose", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {<!-- -->"tom", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {<!-- -->"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {<!-- -->"system", "password", "ROLE_ACTIVITI_USER"},
                {<!-- -->"admin", "password", "ROLE_ACTIVITI_ADMIN"},
        };

        for (String[] user : usersGroupsAndRoles) {<!-- -->
            List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
            logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
            inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
                    authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
        }

        return inMemoryUserDetailsManager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {<!-- -->
        return new BCryptPasswordEncoder();
    }
}

5. Create Bpmn file

Activiti7 can automatically deploy the process. The premise is to create a new directory processes in the resources directory to place the bpmn file.

Create a simple Bpmn process file and set the task’s user group Candidate Groups.

The content in Candidate Groups must be consistent with the user group name appearing in the DemoApplicationConfiguration class above. You can fill in: activitiTeam or otherTeam.

The advantage of filling in this way: When you are not sure who is responsible for the current task, as long as you are a user in Groups, you can pick up the task.

<process id="myProcess" isClosed="false" isExecutable="true" processType="None">
    <startEvent id="_2" name="StartEvent"/>
    <endEvent id="_3" name="EndEvent"/>
    <userTask activiti:candidateGroups="activitiTeam" activiti:exclusive="true" id="_4" name="UserTask"/>
    <userTask activiti:candidateGroups="activitiTeam" activiti:exclusive="true" id="_5" name="UserTask"/>
    <sequenceFlow id="_6" sourceRef="_2" targetRef="_4"/>
    <sequenceFlow id="_7" sourceRef="_4" targetRef="_5"/>
    <sequenceFlow id="_8" sourceRef="_5" targetRef="_3"/>
  </process>

6. Test

@RunWith(SpringRunner.class)
@SpringBootTest
public class AppApplicationTests {<!-- -->

    @Autowired
    private ProcessRuntime processRuntime;

    @Autowired
    private TaskRuntime taskRuntime;

    @Autowired
    private SecurityUtil securityUtil;


    @Test
    public void testApp() {<!-- -->
        System.out.println(taskRuntime);
    }


    /**
     * View process definition
     */
    @Test
    public void contextLoads() {<!-- -->
        securityUtil.logInAs("system");
        Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
        System.out.println("Number of available process definitions:" + processDefinitionPage.getTotalItems());

        for (ProcessDefinition pd : processDefinitionPage.getContent()) {<!-- -->
            System.out.println("Process definition:" + pd);
        }
    }

    @Test
    public void testStartProcess() {<!-- -->
        securityUtil.logInAs("system");
        ProcessInstance pi = processRuntime.start(ProcessPayloadBuilder.start().withProcessDefinitionKey("myProcess").build());
        System.out.println("Process instance ID:" + pi.getId());
    }

    /**
     * Query tasks and complete your own tasks
     */
    @Test
    public void testTask() {<!-- -->
        securityUtil.logInAs("jack");
        Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
        if (tasks.getTotalItems() > 0) {<!-- -->
            for (Task task : tasks.getContent()) {<!-- -->
                taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(task.getId()).build());
                System.out.println("Task:" + task);
                taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build());
            }
        }
        Page<Task> taskPage = taskRuntime.tasks(Pageable.of(0, 10));
        if (taskPage.getTotalItems() > 0) {<!-- -->
            System.out.println("Task:" + taskPage.getContent());
        }
    }
}