Springboot整合Activiti7

前言

今天在springboot项目中完成了对Activiti7的整合,activiti7提供了对springboot的场景启动器(starter),也提供了相应的依赖管理的包,所以整个过程非常的的方便,也比较简单。

但是整合的过程中以为Activiti7默认使用了Spring Security,所以整合的过程中有温习了一遍关于Spring Security的配置信息。Activiti在鉴权方便帮我们做了选择,这有的时候也限制了我们使用其他的库,比如Shiro等,这也可以看出Activiti是提倡我们使用SpringSecurity来做权限认证的。

网上也已经有许多博客讲了如何整合Shiro到Activit中,这个我目前没有这方面的需求所以也没有去做相应的研究,闲暇之时我也回去尝试下如何整合Shiro。

本篇文章主要作为学习笔记记录所用,也希望能帮助希望快速整合Activiti的朋友做个参考,文中不足之处还有望各路大神指出。

鉴权

官方Example中的Util

@Component
public class SecurityUtil {
    // 模拟调用了SpringSecurity 登录鉴权
    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    @Autowired
    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 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);
    }
}

为了方便测试,Activiti的官方示例中提供了一个Util,我们注意到这个仓库进行了模拟用户登录,并将鉴权信息赋值到引擎中:

org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);

新API中的鉴权原理

  • TaskRuntime
package org.activiti.runtime.api.impl;
@PreAuthorize("hasRole('ACTIVITI_USER')")
public class TaskRuntimeImpl implements TaskRuntime {
}

  • ProcessRuntime
package org.activiti.runtime.api.impl;
@PreAuthorize("hasRole('ACTIVITI_USER')")
public class ProcessRuntimeImpl implements ProcessRuntime {
}

activiti7中对原有的一些接口做了二次封装,从而进一步简化了用户的使用流程。

通过查看这个两个API的实现类源码来看,调用的话需要调用的用户含有ACTIVITI_USER角色权限。所以,如果没有使用SpringSecurity的话,这两个API便不能直接调用。

POM文件



    org.springframework.boot
    spring-boot-starter-web
    2.3.3.RELEASE



    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    2.1.4



    mysql
    mysql-connector-java
    runtime
    8.0.19
 


    org.activiti
    activiti-spring-boot-starter
    7.1.0.M4


    org.activiti.dependencies
    activiti-dependencies
    7.1.0.M4
    pom

SpringSecurity简易配置

如果仅仅是测试的话,可以直接将用户存在内存中实现。我这里还是使用的数据库方式来保存用户信息。

  • 用户表结构

    +----------+--------------+------+-----+---------+----------------+
    | Field    | Type         | Null | Key | Default | Extra          |
    +----------+--------------+------+-----+---------+----------------+
    | id       | int          | NO   | PRI | NULL    | auto_increment |
    | username | varchar(255) | YES  |     | NULL    |                |
    | password | varchar(255) | YES  |     | NULL    |                |
    | roles    | varchar(255) | YES  |     | NULL    |                |
    +----------+--------------+------+-----+---------+----------------+
    

需要实现的配置

用户查询接口

  • 实现UserDetails的接口类作为鉴权用户实体
@Component
public class LocalUserDetail implements UserDetails {

    private int id;
    private String username;
    private String password;
    private String roles;
    
    // Entity 转 LocalUserDetail
    public static LocalUserDetail of(User user){
        LocalUserDetail localUserDetail = new LocalUserDetail();
        localUserDetail.id = user.getId();
        localUserDetail.username = user.getUsername();
        localUserDetail.password = user.getPassword();
        localUserDetail.roles = user.getRoles();
        return localUserDetail;
    }
    
    // 权限包装
    @Override
    public Collection getAuthorities() {
        return Arrays.stream(roles.split(",")).map(e->new SimpleGrantedAuthority(e)).collect(Collectors.toSet());
    }


    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 用户查询接口

    @Component
    public class CustomUserDetailService implements UserDetailsService {
    
        @Autowired
        UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 调用DAO实现用户的查询
            Optional user = userMapper.selectOne(c -> c.where(UserDynamicSqlSupport.username, isEqualTo(username)));
            if (!user.isPresent()){
               throw new UsernameNotFoundException("用户不存在");
            }
            User u = user.get();
            return LocalUserDetail.of(u);
        }
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
    }
    

登录成功的处理(可选)

@Component()
public class LoginSuccessHandle implements AuthenticationSuccessHandler {

  @Autowired
  ObjectMapper objectMapper;

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
      System.out.println("浏览器表单登录");
  }

  @Override
  public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
      System.out.println("AJAX登录");
      // 统一返回json体作为回应
      httpServletResponse.setContentType("application/json;charset=UTF-8");        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(BaseResponse.success(UUID.randomUUID().toString())));
  }
}

登录失败的处理(可选)

@Component
public class LoginFailHandle implements AuthenticationFailureHandler {
    @Autowired
    ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 统一返回json体作为回应
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(BaseResponse.error("登录失败,请重试!")));
    }
}

鉴权失效的处理(可选)

/**
 * 检测到未登录的时候,这里返回json的应答,而不是跳转到登录页面
 */
public class AuthEntryPoint implements AuthenticationEntryPoint {

    public static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest httpServletRequest, 
                         HttpServletResponse httpServletResponse,
                         AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        BaseResponse response = new BaseResponse<>(401, "请先登录系统");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(response));
    }
}

SpringSecurity 配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginSuccessHandle successHandle;

    @Autowired
    LoginFailHandle failHandle;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin() 
                .loginProcessingUrl("/login")
                // 配置登录成功处理器
                .successHandler(successHandle)
                // 配置登录失败处理器
                .failureHandler(failHandle)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/v2/**").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .logout().permitAll().and()
                .headers().frameOptions().disable()//让frame页面可以正常使用
                .and()
                // 配置自定义鉴权失败端点
                .exceptionHandling().authenticationEntryPoint(new AuthEntryPoint())
                .and()
                .csrf().disable();
    }
}

Activiti 使用

activiti工作流程图

运行前提

运行后没有生成数据库表

在数据库访问的JDBC URL上添加配置:nullCatalogMeansCurrent=true

在使用mysql-connect 8.+以上版本的时候需要添加nullCatalogMeansCurrent=true参数,否则在使用mybatis-generator生成表对应的xml等时会扫描整个服务器里面的全部数据库中的表,而不是扫描对应数据库的表。因此mysql会扫描所有的库来找表,如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。

调用接口是报缺少字段

Activiti自身问题。

alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL;

流程部署相关

自动加载BPMN文件部署

将bpmn文件放在resource下的processes目录下,activiti启动的时候会自动加载该目录下的bpmn文件

调用接口部署

上传文件部署
@PostMapping("/uploadFileAndDeployment")
public BaseResponse uploadFileAndDeployment(
    @RequestParam("processFile")MultipartFile processFile,
    @RequestParam(value = "processName",required = false) String processName){
    
    String originalFilename = processFile.getOriginalFilename();
    String extension = FilenameUtils.getExtension(originalFilename);
    if (processName != null){
        processName = originalFilename;
    }
    try {
        InputStream inputStream = processFile.getInputStream();
        Deployment deployment = null;
        if ("zip".equals(extension)){
            // 压缩包部署方式
            ZipInputStream zipInputStream = new ZipInputStream(inputStream);
            deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();
        }else if ("bpmn".equals(extension)){
            // bpmn文件部署方式
            deployment = repositoryService.createDeployment().addInputStream(originalFilename,inputStream).name(processName).deploy();
        }
        return BaseResponse.success(deployment);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return BaseResponse.success();
}
上传BPMN内容字符串部署
@PostMapping("/postBPMNAndDeployment")
public BaseResponse postBPMNAndDeployment(@RequestBody AddXMLRequest addXMLRequest){
    Deployment deploy = repositoryService.createDeployment()
        // .addString 第一次参数的名字如果没有添加.bpmn的话,不会插入到 ACT_RE_DEPLOYMENT 表中
        .addString(addXMLRequest.getProcessName()+".bpmn", addXMLRequest.getBpmnContent())
        .name(addXMLRequest.getProcessName())
        .deploy();
    return BaseResponse.success(deploy);
}
获取流程资源文件
@GetMapping("/getProcessDefineXML")
public void getProcessDefineXML(String deploymentId, String resourceName, HttpServletResponse response){
    try {
        InputStream inputStream = repositoryService.getResourceAsStream(deploymentId,resourceName);
        int count = inputStream.available();
        byte[] bytes = new byte[count];
        response.setContentType("text/xml");
        OutputStream outputStream = response.getOutputStream();
        while (inputStream.read(bytes) != -1) {
            outputStream.write(bytes);
        }
        inputStream.close();
    } catch (Exception e) {
        e.toString();
    }
}

流程实例相关

启动实例

@PostMapping("/startProcess")
public BaseResponse startProcess(
    String processDefinitionKey, 
    String instanceName,
    @AuthenticationPrincipal LocalUserDetail userDetail){
    
    ProcessInstance processInstance = null;
    try{
        StartProcessPayload startProcessPayload = ProcessPayloadBuilder.start().withProcessDefinitionKey(processDefinitionKey)
            .withBusinessKey("businessKey")
            .withVariable("sponsor",userDetail.getUsername())
            .withName(instanceName).build();
        processInstance = processRuntime.start(startProcessPayload);
    }catch (Exception e){
        System.out.println(e);
        return BaseResponse.error("开启失败:"+e.getLocalizedMessage());
    }
    return BaseResponse.success(processInstance);
}

挂起实例

@PostMapping("/suspendInstance/{instanceId}")
public BaseResponse suspendInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder.suspend().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
}

激活实例

@PostMapping("/resumeInstance/{instanceId}")
public BaseResponse resumeInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime
        .resume(ProcessPayloadBuilder.resume().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
}

任务相关接口

完成任务

@PostMapping("/completeTask/{taskId}")
public BaseResponse completeTask(@PathVariable String taskId){
    Task task = taskRuntime.task(taskId);
    if (task.getAssignee()==null){
        // 说明任务需要拾取
        taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
    }
    taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
    return BaseResponse.success();
}

获取自己的任务

@GetMapping("/getTasks")
public BaseResponse getTasks(){
    Page taskPage = taskRuntime.tasks(Pageable.of(0, 100));
    List tasks = taskPage.getContent();
    List taskVOS = new ArrayList<>();
    for (Task task : tasks) {
        TaskVO taskVO = TaskVO.of(task);
        ProcessInstance instance = processRuntime.processInstance(task.getProcessInstanceId());
        taskVO.setInstanceName(instance.getName());
        taskVOS.add(taskVO);
    }
    return BaseResponse.success(taskVOS);
}

历史数据查询

public List getProcessHistoryByBusinessKey(String businessKey) {
    ProcessInstance instance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult();
    List historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.getId())
        .orderByHistoricActivityInstanceStartTime().asc().list();
    List historicActivityInstanceVOList = new ArrayList<>();
    historicActivityInstanceList.forEach(historicActivityInstance -> historicActivityInstanceVOList.add(VOConverter.getHistoricActivityInstanceVO(historicActivityInstance)));
    return historicActivityInstanceVOList;
}  

历史详情查询

HistoricDetailQuery historicDetailQuery = historyService.createHistoricDetailQuery();
List historicDetails = historicDetailQuery.processInstanceId(instanceId).orderByTime().list();
for (HistoricDetail hd: historicDetails) {
    System.out.println("流程实例ID:"+hd.getProcessInstanceId());
    System.out.println("活动实例ID:"+hd.getActivityInstanceId());
    System.out.println("执行ID:"+hd.getTaskId());
    System.out.println("记录时间:"+hd.getTime());
}

历史流程实例查询

HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
List processInstances = historicProcessInstanceQuery.processDefinitionId(processDefinitionId).list();
for (HistoricProcessInstance hpi : processInstances) {
    System.out.println("业务ID:"+hpi.getBusinessKey());
    System.out.println("流程定义ID:"+hpi.getProcessDefinitionId());
    System.out.println("流程定义Key:"+hpi.getProcessDefinitionKey());
    System.out.println("流程定义名称:"+hpi.getProcessDefinitionName());
    System.out.println("流程定义版本:"+hpi.getProcessDefinitionVersion());
    System.out.println("流程部署ID:"+hpi.getDeploymentId());
    System.out.println("开始时间:"+hpi.getStartTime());
    System.out.println("结束时间:"+hpi.getEndTime());
}
package org.activiti.engine.history; 
@Internal
public interface HistoricProcessInstance {
    String getId();

    String getBusinessKey();

    String getProcessDefinitionId();

    String getProcessDefinitionName();

    String getProcessDefinitionKey();

    Integer getProcessDefinitionVersion();

    String getDeploymentId();

    Date getStartTime();

    Date getEndTime();

    Long getDurationInMillis();

    String getEndActivityId();

    String getStartUserId();

    String getStartActivityId();

    String getDeleteReason();

    String getSuperProcessInstanceId();

    String getTenantId();

    String getName();

    String getDescription();

    Map getProcessVariables();
}

任务历史查询

某一次流程的执行经历的多少任务

HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
List taskInstances = historicTaskInstanceQuery.taskId(taskId).list();
for (HistoricTaskInstance hti : taskInstances) {
    System.out.println("开始时间:"+hti.getStartTime());
    System.out.println("结束时间:"+hti.getEndTime());
    System.out.println("任务拾取时间:"+hti.getClaimTime());
    System.out.println("删除原因:"+hti.getDeleteReason());
}

活动历史查询

查询某个流程的每个阶段(活动)

HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService.createHistoricActivityInstanceQuery();
List historicActivityInstances = historicActivityInstanceQuery.processInstanceId(instanceId).list();
for (HistoricActivityInstance hai : historicActivityInstances) {
    System.out.println("活动ID:"+hai.getActivityId());
    System.out.println("活动类型:"+hai.getActivityType());
    System.out.println("活动名称:"+hai.getActivityName());
    System.out.println("任务ID:"+hai.getTaskId());
}
package org.activiti.engine.history;  
@Internal
public interface HistoricActivityInstance extends HistoricData {
    String getId();

    String getActivityId();

    String getActivityName();

    String getActivityType();

    String getProcessDefinitionId();

    String getProcessInstanceId();

    String getExecutionId();

    String getTaskId();

    String getCalledProcessInstanceId();

    String getAssignee();

    Date getStartTime();

    Date getEndTime();

    Long getDurationInMillis();

    String getDeleteReason();

    String getTenantId();
}

变量历史信息

某一次流程的执行时设置的流程变量

HistoricVariableInstanceQuery historicVariableInstanceQuery = historyService.createHistoricVariableInstanceQuery();
List variableInstances =historicVariableInstanceQuery
                                                    .processInstanceId(instanceId)
                                                    .list();
for (HistoricVariableInstance hva : variableInstances) {
    System.out.println("变量名称:"+hva.getVariableName());
    System.out.println("变量类型名称:"+hva.getVariableTypeName());
    System.out.println("变量值:"+hva.getValue());
    System.out.println("流程实例ID:"+hva.getProcessInstanceId());
    System.out.println("任务ID:"+hva.getTaskId());
}

附件

Activiti官方示例源码

本章源码码云仓库

你可能感兴趣的:(Springboot整合Activiti7)