SpringBoot+flowable-ui集成实战

前言

公司最近想要使用flowable作为我们工作流引擎,主要用于各类流程审批上。在网上搜索到了很多有参考意义的文章,但有些实现细节还需要自己去摸索。本文的实战包括:

  1. 在项目中引入flowable的包可以使用flowable的api;
  2. 将flowable-ui集成到自己项目里;
  3. 如何使用flowable-ui创建的流程模型(我们用的是bpmn模型,所以后面的提到的流程都是指bpmn);
  4. 集成公司的用户认证体系;
  5. 自动分配

sample源码地址: https://github.com/ChangeChe/flowable

flowable集成

flowable的集成有两部分:flowable-api与flowable-ui。flowable-api主要管创建好流程后怎么使用,flowable-ui就是通过页面去编排我们的流程。

jar包引入

这里的是较新的版本6.7.0。因为自身的框架用的是springboot,所以需要引入一个starter。引入的包里有flowable-ui-xxx是flowable-ui需要的包;其他是我们使用flowable-api需要的包。


    6.7.0


    
        org.flowable
        flowable-spring-boot-starter-process
        ${flowable.version}
    
    
        org.flowable
        flowable-idm-spring-configurator
        ${flowable.version}
    
    
        org.flowable
        flowable-json-converter
        ${flowable.version}
    
    
        org.flowable
        flowable-ui-modeler-rest
        ${flowable.version}
        
            
                org.flowable
                flowable-json-converter
            
            
                org.springframework.boot
                spring-boot-starter-log4j2
            
        
    
    
        org.flowable
        flowable-ui-modeler-conf
        ${flowable.version}
        
            
                org.flowable
                flowable-json-converter
            
        
    
    
        org.flowable
        flowable-ui-idm-conf
        ${flowable.version}
    
    
        org.flowable
        flowable-ui-idm-rest
        ${flowable.version}
    
    
        org.flowable
        flowable-form-engine
        ${flowable.version}
    
    
        org.flowable
        flowable-form-spring
        ${flowable.version}
    
    
        org.flowable
        flowable-form-engine-configurator
        ${flowable.version}
    
    
        org.flowable
        flowable-form-spring-configurator
        ${flowable.version}
    
    
        org.flowable
        flowable-app-engine
        ${flowable.version}
    
    
        org.liquibase
        liquibase-core
        4.3.5
        
            
                javax.xml.bind
                jaxb-api
            
        
    

配置

# flowable配置
flowable.common.app.idm-url = /idm
flowable.modeler.app.rest-enabled = true
flowable.database-schema-update = true
flowable.process.definition-cache-limit = 1
flowable.xml.encoding = UTF-8

# mybatis配置
mybatis.mapper-locations = classpath:/META-INF/modeler-mybatis-mappings/*.xml
mybatis.config-location = classpath:/META-INF/mybatis-config.xml
mybatis.configuration-properties.blobType = BLOB
mybatis.configuration-properties.boolValue = TRUE
mybatis.configuration-properties.prefix = 

Web配置

路由

@Configuration
@EnableConfigurationProperties({FlowableIdmAppProperties.class, FlowableModelerAppProperties.class})
@ComponentScan(basePackages = {
        "org.flowable.ui.idm.conf",
//        "org.flowable.ui.idm.security",
        "org.flowable.ui.idm.service",
        "org.flowable.ui.modeler.repository",
        "org.flowable.ui.modeler.service",
//        "org.flowable.ui.common.filter",
        "org.flowable.ui.common.service",
        "org.flowable.ui.common.repository",
//        "org.flowable.ui.common.security",
        "org.flowable.ui.common.tenant",
        "org.flowable.form"
}, excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = org.flowable.ui.idm.conf.ApplicationConfiguration.class)
})
public class ApplicationConfiguration implements BeanPostProcessor {
    @Bean
    public ServletRegistrationBean apiServlet(ApplicationContext applicationContext) {
        AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
        dispatcherServletConfiguration.setParent(applicationContext);
        dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class);
        DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration);
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/api/*");
        registrationBean.setName("Flowable IDM App API Servlet");
        registrationBean.setLoadOnStartup(1);
        registrationBean.setAsyncSupported(true);
        return registrationBean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 这里与用户鉴权相关 这里先省略
        ...
    }
}

i18n

@Configuration
@ComponentScan(basePackages = {
        "org.flowable.ui.idm.rest.app",
        "org.flowable.ui.common.rest.exception",
        "org.flowable.ui.modeler.rest.app",
        "org.flowable.ui.common.rest"
}, excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = RemoteAccountResource.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StencilSetResource.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorUsersResource.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorGroupsResource.class)
})
@Slf4j
public class AppDispatcherServletConfiguration implements WebMvcRegistrations {
    @Bean
    public SessionLocaleResolver localeResolver() {
        return new SessionLocaleResolver();
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        log.debug("Configuring localeChangeInterceptor");
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        log.debug("Creating requestMappingHandlerMapping");
        RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping();
        requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
        requestMappingHandlerMapping.setRemoveSemicolonContent(false);
        Object[] interceptors = {localeChangeInterceptor()};
        requestMappingHandlerMapping.setInterceptors(interceptors);
        return requestMappingHandlerMapping;
    }
}

页面集成

路由集成后,我们需要将资源文件放到我们的resource目录下。首先需要将路由跟资源路径绑定:

@Configuration
public class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/modeler/**")
                .addResourceLocations("classpath:/static/modeler/");
        registry.addResourceHandler("/idm/**")
                .addResourceLocations("classpath:/static/idm/");
    }
}

然后从flowable-engine源码中将前端资源拷贝到自己的项目下:

  1. 源码地址:https://github.com/flowable/flowable-engine
  2. 进入modules/flowable-ui目录
  3. 拷贝flowable-idm-frontendflowable-modeler-frontendflowable-task-frontend到自己的项目下,如下图:
    SpringBoot+flowable-ui集成实战_第1张图片

数据库初始化

@Configuration
@Slf4j
public class DatabaseConfiguration {
    protected static final String LIQUIBASE_CHANGELOG_PREFIX = "ACT_DE_";
    
    @Bean
    public Liquibase liquibase(DataSource dataSource) {
        log.info("Configuring Liquibase");

        Liquibase liquibase = null;
        try {
            DatabaseConnection connection = new JdbcConnection(dataSource.getConnection());
            Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
            database.setDatabaseChangeLogTableName(LIQUIBASE_CHANGELOG_PREFIX + database.getDatabaseChangeLogTableName());
            database.setDatabaseChangeLogLockTableName(LIQUIBASE_CHANGELOG_PREFIX + database.getDatabaseChangeLogLockTableName());

            liquibase = new Liquibase("META-INF/liquibase/flowable-modeler-app-db-changelog.xml", new ClassLoaderResourceAccessor(), database);
            liquibase.update("flowable");
            return liquibase;
        } catch (Exception e) {
            throw new InternalServerErrorException("Error creating liquibase database", e);
        } finally {
            closeDatabase(liquibase);
        }
    }

    private void closeDatabase(Liquibase liquibase) {
        if (liquibase != null) {
            Database database = liquibase.getDatabase();
            if (database != null) {
                try {
                    database.close();
                } catch (DatabaseException e) {
                    log.warn("Error closing database", e);
                }
            }
        }
    }
}

经过上述步骤,我们已经能在自己的项目里启动flowable-ui了。

集成用户中心

使用flowable-idm进行用户授权不满足我们的使用需求,idm也提供了ldap的登录方式;在这里,我们使用自己的用户中心进行登录,我们的用户中心对外提供了登录/用户鉴权接口。参考了LDAP提供的JavaConfig(FlowableLdapAutoConfiguration)知道我们需要实现自己的IdmIdentityServiceImpl以及UserDetailsService

IdmIdentityServiceImpl

@Slf4j
public class MyIdentityServiceImpl extends IdmIdentityServiceImpl {

    MyUserCenterProperties properties;

    ApplicationContext applicationContext;

    public MyIdentityServiceImpl(ApplicationContext applicationContext, MyUserCenterProperties properties, IdmEngineConfiguration idmEngineConfiguration) {
        super(idmEngineConfiguration);
        this.applicationContext = applicationContext;
        this.properties = properties;
    }

    @Override
    public boolean checkPassword(String userId, String password) {
        return executeCheckPassword(userId, password);
    }

    protected boolean executeCheckPassword(final String userId, final String password) {
        // Extra password check, see http://forums.activiti.org/comment/22312
        if (password == null || password.length() == 0) {
            throw new FlowableException("Null or empty passwords are not allowed!");
        }

        try {
            // 调用用户中心服务进行登录
            UserCenterFeignService userCenterFeignService = applicationContext.getBean(UserCenterFeignService.class);
            userCenterFeignService.login(new UcLoginReq(properties.getAppId(), userId, password));
            return true;
        } catch (DecodeException e) {
            log.error("用户中心认证失败:{}", userId, e);
            return false;
        } catch (FlowableException e) {
            log.error("Could not authenticate user : {}", userId, e);
            return false;
        }
    }

    // 很多方法直接不支持 
    // throw new FlowableException("My identity service doesn't support getting groups with privilege");
    ...

    @Override
    public UserQuery createUserQuery() {
        // 通过调用其他服务来查询
        SoaCommonPhpFeignService soaCommonPhpFeignService = applicationContext.getBean(SoaCommonPhpFeignService.class);
        SoaCommonJavaFeignService soaCommonJavaFeignService = applicationContext.getBean(SoaCommonJavaFeignService.class);
        return new MyUserQueryImpl(soaCommonPhpFeignService, soaCommonJavaFeignService);
    }

    // 这个在对任务查询时需要针对user_id/group_id查询时需要
    @Override
    public GroupQuery createGroupQuery() {
        return new MyGroupQueryImpl();
    }
}

public class MyUserQueryImpl extends UserQueryImpl {

    SoaCommonPhpFeignService soaCommonPhpFeignService;
    SoaCommonJavaFeignService soaCommonJavaFeignService;

    public MyUserQueryImpl(SoaCommonPhpFeignService soaCommonPhpFeignService, SoaCommonJavaFeignService soaCommonJavaFeignService) {
        this.soaCommonPhpFeignService = soaCommonPhpFeignService;
        this.soaCommonJavaFeignService = soaCommonJavaFeignService;
    }

    @Override
    public long executeCount(CommandContext commandContext) {
        return executeQuery().size();
    }

    @Override
    public List executeList(CommandContext commandContext) {
        return executeQuery();
    }

    protected List executeQuery() {
        if (getId() != null) {
            List result = new ArrayList<>();
            UserEntity user = findById(getId());
            if (user != null) {
                result.add(user);
            }
            return result;

        } else if (getIdIgnoreCase() != null) {
            List result = new ArrayList<>();
            UserEntity user = findById(getIdIgnoreCase());
            if (user != null) {
                result.add(user);
            }
            return result;

        } else if (getFullNameLike() != null) {
            return executeNameQuery(getFullNameLike());

        } else if (getFullNameLikeIgnoreCase() != null) {
            return executeNameQuery(getFullNameLikeIgnoreCase());

        } else {
            return executeAllUserQuery();
        }
    }
    
    // 我们的使用场景并不需要这些返回值,所以都返回null
    protected List executeNameQuery(String name) {
        return null;
    }

    protected List executeAllUserQuery() {
        return null;
    }

    protected UserEntity findById(final String userId) {
        return null;
    }
}

public class MyGroupQueryImpl extends GroupQueryImpl {
    @Override
    public List executeList(CommandContext commandContext) {
        // 也不需要这里的值,只是为了在执行其他逻辑时不报错
        return new ArrayList<>();
    }
}

UserDetailsService

@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    SoaCommonPhpFeignService soaCommonPhpFeignService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 去别的服务查询用户 MySoaEmployeeDetailResp需要实现UserDetails接口
        MySoaEmployeeDetailResp employeeByAccount = soaCommonPhpFeignService.getEmployeeByAccount(s);
        if (null == employeeByAccount) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return employeeByAccount;
    }
}

配置我们自己的JavaConfig

@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore({
        FlowableUiSecurityAutoConfiguration.class,
        FlowableSecurityAutoConfiguration.class,
        IdmEngineServicesAutoConfiguration.class,
        ProcessEngineServicesAutoConfiguration.class,
        IdmSecurityConfiguration.class
})
@EnableConfigurationProperties({
        MyUserCenterProperties.class
})
public class MyUserCenterAutoConfiguration implements ApplicationContextAware {
    protected final MyUserCenterProperties properties;

    ApplicationContext applicationContext;

    public MyUserCenterAutoConfiguration(MyUserCenterProperties properties) {
        this.properties = properties;
    }

    @Bean
    public EngineConfigurationConfigurer myIdmEngineConfigurer() {
        return idmEngineConfiguration -> idmEngineConfiguration
                .setIdmIdentityService(new MyIdentityServiceImpl(applicationContext, properties, idmEngineConfiguration));
    }

    @Bean
    public FlowableAuthenticationProvider flowableAuthenticationProvider(IdmIdentityService idmIdentitySerivce, @Qualifier("myUserDetailsServiceImpl") UserDetailsService userDetailsService) {
        return new FlowableAuthenticationProvider(idmIdentitySerivce, userDetailsService);
    }

    @Bean
    @ConditionalOnMissingBean
    public RememberMeServices flowableUiRememberMeService(FlowableCommonAppProperties properties, @Qualifier("myUserDetailsServiceImpl") UserDetailsService userDetailsService, PersistentTokenService persistentTokenService) {
        return new CustomPersistentRememberMeServices(properties, userDetailsService, persistentTokenService);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

从model到repository

经过上述步骤,我们可以用我们自己的账号登录flowable-ui并通过可视化工具编排流程了。但在我们通过flowable-api开始一个流程时,它会告诉我们这个流程不存在。这是怎么回事呢?

这是因为org.flowable.engine.RuntimeService#startProcessInstanceXXX是要在act_re_procdef表里找到流程定义,而我们通过flowable-ui定义的流程还在act_de_model里。那么怎么将act_de_model里的数据同步到act_re_procdef去呢?

flowable给我们提供了org.flowable.engine.RepositoryService#createDeployment这个api,通过这个api我们就能将act_de_model里的数据同步到act_re_procdef去了;同理,还有flowable-ui里的表单模型也可以通过org.flowable.form.api.FormRepositoryService#createDeployment同步到act_fo_form_definition里去。

这里用了定时器执行这个规则:

import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.form.api.FormDeployment;
import org.flowable.form.api.FormRepositoryService;
import org.flowable.ui.modeler.domain.AbstractModel;
import org.flowable.ui.modeler.serviceapi.ModelService;

@Configuration
public class ModelActiveJob {

    @Autowired
    ModelService modelService;

    @Autowired
    RepositoryService repositoryService;

    @Autowired
    FormRepositoryService formRepositoryService;

    @Autowired
    ITaskService taskService;

    /**
     * 激活流程定义
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void activeProcessDefinition() {
        // 查找BPMN模型
        List modelsByModelType = modelService.getModelsByModelType(ModelTypeConst.BPMN);
        for (AbstractModel model: modelsByModelType) {
            Date modelLastUpdated = model.getLastUpdated();
            // 查询该流程模型最新的发布记录
            List list = repositoryService.createDeploymentQuery().deploymentKey(model.getKey()).latest().list();
            boolean deploy = true;
            if (list.size() > 0) {
                Deployment deployment = list.get(0);
                Date deploymentTime = deployment.getDeploymentTime();
                // 如果模型的更新时间比最新的发布时间小,说明没有更新
                if (modelLastUpdated.compareTo(deploymentTime) < 0) {
                    deploy = false;
                }
            }
            // 发布
            if (deploy) {
                BpmnModel bpmnModel = modelService.getBpmnModel(model);
                repositoryService.createDeployment().name(model.getName()).key(model.getKey()).addBpmnModel(model.getKey() + ".bpmn", bpmnModel).deploy();
            }
        }
    }

    /**
     * 激活表单
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void activeFormDefinition() {
        // 查找FORM模型
        List modelsByModelType = modelService.getModelsByModelType(ModelTypeConst.FORM);
        for (AbstractModel model: modelsByModelType) {
            Date modelLastUpdated = model.getLastUpdated();
            List list = formRepositoryService.createDeploymentQuery().formDefinitionKey(model.getKey()).list();

            boolean deploy = true;
            if (list.size() > 0) {
                list.sort((o1, o2) -> o2.getDeploymentTime().compareTo(o1.getDeploymentTime()));
                // 查询该表单模型最新的发布记录
                FormDeployment formDeployment = list.get(0);
                Date deploymentTime = formDeployment.getDeploymentTime();
                // 如果模型的更新时间比最新的发布时间小,说明没有更新
                if (modelLastUpdated.compareTo(deploymentTime) < 0) {
                    deploy = false;
                }
            }
            // 发布
            if (deploy) {
                formRepositoryService.createDeployment().name(model.getName()).addFormDefinition(model.getKey() + ".form", model.getModelEditorJson()).deploy();
            }
        }
    }
}

动态数据源

在审批执行过程中,可能需要查询我们自己的业务数据库,对一些操作进行约束或者怎么地;因为flowable是一个基础服务,我们不可能把flowable的相关数据表放到业务数据库里,所以就需要在项目里使用多数据源。

我们使用的是mybatis-plus作为我们的数据库工具,它提供了@DS让我们在多数据源的情况下可以进行数据源切换,但这有一个问题,就是在一个服务里使用用两个数据源是会出现误读的情况的:

db1.tableA not exists

而像我们实现的一些监听器,都是在flowable里访问我们的业务数据库,所以这种方式不满足我们的需求,需要重新制定规则。

动态数据源

动态数据源的配置很简单

spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.dynamic.primary = flowable
spring.datasource.dynamic.datasource.flowable.username = 
spring.datasource.dynamic.datasource.flowable.password = 
spring.datasource.dynamic.datasource.flowable.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.flowable.url = 
spring.datasource.dynamic.datasource.flowable.druid.initial-size = 10
spring.datasource.dynamic.datasource.flowable.druid.max-active = 100
spring.datasource.dynamic.datasource.flowable.druid.min-idle = 10
spring.datasource.dynamic.datasource.flowable.druid.max-wait = 10
spring.datasource.dynamic.datasource.biz.username = 
spring.datasource.dynamic.datasource.biz.password = 
spring.datasource.dynamic.datasource.biz.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.biz.url =
spring.datasource.dynamic.datasource.biz.druid.initial-size = 10
spring.datasource.dynamic.datasource.biz.druid.max-active = 100
spring.datasource.dynamic.datasource.biz.druid.min-idle = 10
spring.datasource.dynamic.datasource.biz.druid.max-wait = 10
@Configuration
@Slf4j
public class DatabaseConfiguration {
    protected static final String LIQUIBASE_CHANGELOG_PREFIX = "ACT_DE_";

    /**
     * flowable数据源
     */
    @Bean
    @ConfigurationProperties("spring.datasource.dynamic.datasource.flowable")
    @Primary
    public DataSource flowableDataSource(){
        log.info("加载主数据源flowable DataSource.");
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 业务
     */
    @Bean
    @ConfigurationProperties("spring.datasource.dynamic.datasource.biz")
    public DataSource bizDataSource(){
        log.info("加载从数据源biz DataSource.");
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 动态数据源
     */
    @Bean
    public DataSource myRoutingDataSource(@Qualifier("flowableDataSource") DataSource flowableDataSource,
                                          @Qualifier("bizDataSource") DataSource courseDataSource) {
        log.info("加载[flowableDataSource-bizDataSource]设置为动态数据源DynamicDataSource.");
        Map targetDataSources = new HashMap<>(2);
        targetDataSources.put(DBTypeEnum.FLOWABLE, flowableDataSource);
        targetDataSources.put(DBTypeEnum.BIZ, courseDataSource);

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(flowableDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);

        return dynamicDataSource;
    }

    // 注意 这里需要指定dataSource为flowable datasource
    @Bean
    public Liquibase liquibase(@Qualifier("flowableDataSource") DataSource dataSource) {
        ...
    }
}

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.get();
    }
}

@Slf4j
public class DbContextHolder {
    private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();

    public static void set(DBTypeEnum dbType) {
        log.debug("切换到{}", dbType.name());
        CONTEXT_HOLDER.set(dbType);
    }

    public static DBTypeEnum get() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清除上下文数据
     */
    static void clearDbType() {
        CONTEXT_HOLDER.remove();
    }

}

进行到这一步可以感觉到跟Mybatis读写分离很像。后续也差不多,创建一个org.apache.ibatis.plugin.Interceptor并将其设置到com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;不同的就是org.apache.ibatis.plugin.Interceptor里的规则不同。在读写分离中,我们根据sql里的关键字(UPDATESELECTDELETE…)来切换,这里我们怎么区分呢?

目前我们这边的实现方式是在Mapper类上打上一个注解,声明这个Mapper要用哪个库,再在Interceptor里拦截进行切换。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DBType {
    DBTypeEnum value() default DBTypeEnum.FLOWABLE;
}
@DBType(DBTypeEnum.BIZ)
public interface AuthorityDepartmentMapper extends BaseMapper {
    ...
}
import liquibase.util.StringUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {
                MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = {
                MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class }),
        @Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class DbSelectorInterceptor implements Interceptor {

    private static final Map CACHE_MAP = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        String closeMethodName = "close";
        DBTypeEnum databaseType = null;
        if(!closeMethodName.equals(methodName)) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];
            if((databaseType = CACHE_MAP.get(ms.getId())) == null) {
                // statementId的组成为xxx.xxx.xxMapper.funcName
                String statementId = ms.getId();
                String[] parts = statementId.split("\\.");
                String[] strings = Arrays.copyOf(parts, parts.length - 1);
                String className = StringUtil.join(strings, ".");
                Class aClass = Class.forName(className);
                DBType annotation = aClass.getAnnotation(DBType.class);
                // 默认使用FLOWABLE库
                if (null == annotation) {
                    databaseType = DBTypeEnum.FLOWABLE;
                } else {
                    databaseType = annotation.value();
                }
                log.debug("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), databaseType.name(), ms.getSqlCommandType().name());
                CACHE_MAP.put(ms.getId(), databaseType);
            }

        } else {
            log.debug("close方法 重置为 [{}] Strategy", DBTypeEnum.FLOWABLE.name());
            databaseType = DBTypeEnum.FLOWABLE;
        }
        DbContextHolder.set(databaseType);

        return invocation.proceed();
    }
    ...
}

至此,数据库切换也搞定了~

自动分配

任务节点的自动分配是我们对flowable的重要增强。目标是能根据现有的分配规则进行抽象,通过在flowable-ui上配置的方式来指定分配规则。

根据我们现有的规则总结了这几类分配类型:

类型 说明
fixed-user 指定用户
user-major 流程提交者的部门主管
fixed-department 指定部门-部门下的所有人
fixed-department-major 指定部门主管


这里只是举个例子,分配类型是根据自己公司实际的业务规则抽象的。

有了分配类型后,怎么实现自动分配呢?

这里是在任务的任务监听器create事件监听去进行分配的。
SpringBoot+flowable-ui集成实战_第2张图片

import org.flowable.common.engine.impl.el.FixedValue;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.task.service.delegate.DelegateTask;

public abstract class AbstractAssignDelegate implements AssignDelegate, TaskListener {

    /**
     * 自动分配类型
     */
    FixedValue assignType;
    /**
     * 自动分配依据
     */
    FixedValue assignValue;

    @Override
    public void notify(DelegateTask delegateTask) {
        TaskService taskService = SpringUtil.getBean(TaskService.class);
        List list = taskService.createTaskQuery().taskId(delegateTask.getId()).list();
        if (list.size() < 1) {
            return;
        }
        Task task = list.get(0);
        autoAssign(task.getProcessInstanceId(), task);
    }

    /**
     * 自动分配
     * @param processInstanceId
     * @param taskId
     */
    protected void autoAssign(String processInstanceId, Task task) {
        ...
        String taskId = task.getId();
        if (null != userId) {
            taskService.addCandidateUser(taskId, userId);
        } else if (null != department) {
            taskService.addCandidateGroup(taskId, department);
        } else if (null != role) {
            taskService.addCandidateGroup(taskId, role);
        }
        ...
    }
}
import org.flowable.common.engine.impl.el.FixedValue;

import java.util.ArrayList;
import java.util.List;

/**
 * 除fixed-user会保存userId外,其他方式都是保存的部门-角色信息,便于数据扩展
 * @author Chenjing
 */
public interface AssignDelegate {

    /**
     * 角色存储前缀
     * 部门与角色都存在groupId属性中,通过前缀区分
     */
    static final String ROLE_PREFIX = "$$";

    /**
     * 指定用户
     * @param fixedUser
     * @return
     */
    default String getFixedUser(FixedValue fixedUser) {return fixedUser.getExpressionText();}

    /**
     * 指定部门
     * @param fixedDepartment
     * @return
     */
    default String getFixedDepartment(FixedValue fixedDepartment) {return fixedDepartment.getExpressionText();}

    /**
     * 指定角色
     * @param fixedRole
     * @return
     */
    default String getFixedRole(FixedValue fixedRole) {return ROLE_PREFIX + fixedRole.getExpressionText();}
     ...
}

我们利用了taskidentitylinkact_hi_identitylink表)。约定type=candidate的user/group有审批权限,type=participant有查看权限。

你可能感兴趣的:(菜鸡进阶之路,spring,workflow)