Skip to content

SPI 实现指南 ​

jeeflow 通过 SPI(Service Provider Interface)解耦框架依赖。您只需实现以下接口并注册到 ServiceContext。

注册方式 ​

java
Configuration config = new Configuration();
ServiceContext.put("repository", new MyRepository());
ServiceContext.put("json", new MyJsonProvider());
// ...

JeeflowEngine engine = new JeeflowEngineImpl();
engine.configure(config);

Spring Boot 项目中,SPI 自动装配。如需覆盖,只需声明同名 Bean。


IProcessRepository(必须) ​

聚合仓储——引擎所有持久化操作都通过此接口。

引擎运行时方法 ​

java
public interface IProcessRepository {
    // 流程定义
    ProcessDefine findDefineById(Long defineId);

    // 流程实例
    ProcessInstance findInstanceById(Long instanceId);
    void saveInstance(ProcessInstance instance);
    void updateInstance(ProcessInstance instance);

    // 流程任务
    ProcessTask findTaskById(Long taskId);
    void saveTask(ProcessTask task);
    void updateTask(ProcessTask task);
    List<ProcessTask> findDoingTasks(Long instanceId, String[] taskNames);
    List<ProcessTask> findDoneTasks(Long instanceId, String[] taskNames);
    List<ProcessTask> findHistoryTasks(Long instanceId);

    // 参与者
    List<String> findTaskActors(Long taskId);
    void addTaskActor(Long taskId, List<String> actors);
    void removeTaskActor(Long taskId, List<String> actors);

    // 抄送
    void createCcInstance(Long instanceId, String creator, String... actorIds);
    void updateCcStatus(Long instanceId, String actorId);

    // 抄送写侧判重(issues/141 G2,default 方法:不覆写=维持旧行为)
    default List<String> findCcActorIds(Long instanceId);
    default List<String> createCcInstanceIfAbsent(Long instanceId, String creator, String... actorIds);
}

抄送写侧判重=幂等空操作(G2 · spec 06 §4):同一 (实例, 被抄送人) 已有 cc 行时 跳过——不新增行、不重置未读、不更新原行时间,也不 fire CC_CREATE(码 4)。建 cc 的三条入口 (发起 f_ccActors/办理 tf_ccActors/手动 createCCInstance)统一调 createCcInstanceIfAbsent,逐人 fire 的入参=它返回的实际新建子集。 查询侧不引入 DISTINCT、历史重复行不清理。自带两仓(JDBC / 内存)都覆写 findCcActorIds, 集成方自实现仓储时也应覆写,否则判重不生效(default 返回空集=不判重)。

前端分页查询方法 ​

java
    // 待办/已办
    PageResult<TaskRow> pageTodoTasks(PageQuery query);
    PageResult<TaskRow> pageDoneTasks(PageQuery query);

    // 实例/抄送/定义
    PageResult<InstanceRow> pageInstances(PageQuery query);
    PageResult<InstanceRow> pageCcInstances(PageQuery query);
    PageResult<DefineRow> pageDefines(PageQuery query);

    int countTodoTasks(Long userId);
}

pageCcInstances 的归属条件必填(G1 · spec 06 §2.5):查询必须带 cc.actor_id 的有效条件(非 null、非空串/空白、非空集合)。条件缺失或为空值 ⇒ 返回空页, 严禁退化成"这条条件不加"而放出全部实例;SQL 仓与内存仓必须给同一个答案。 门面 processInstance/ccList 恒挂这条条件,这里防的是绕过门面直连仓储的调用方。

PageQuery ​

java
PageQuery query = new PageQuery(pageNum, pageSize);
query.add("t.task_name", "EQ", "task1");         // 等于
query.add("t.display_name", "LIKE", "审批");       // 模糊
query.add("t.create_time", "GT", startDate);       // 大于
query.add("pta.actor_id", "EQ", userId);           // 参与者
query.setOrderBy("t.id desc");

支持的操作符: EQ, NE, LIKE, LLIKE, RLIKE, GT, GE, LT, LE, IN, NIN, BT

Spring Boot 中的 m_* 参数转换 ​

前端传参使用 m_{alias}_{type}_{column} 约定。JeeflowQueryParser 自动转换:

java
@Autowired
private JeeflowQueryParser queryParser;

@PostMapping("/wf/processTask/todoList")
public CommonResult<CommonPage<TaskRow>> todoList(@RequestBody Map<String, Object> params) {
    PageQuery query = queryParser.parse(params);
    query.add("pta.actor_id", "EQ", currentUserId);
    return CommonResult.data(repository.pageTodoTasks(query));
}

前端传参示例:

json
{
  "pageNum": 1,
  "pageSize": 10,
  "m_t_EQ_taskName": "apply",
  "m_t_LIKE_displayName": "审批",
  "m_pi_GT_createTime": "2026-01-01"
}

表别名约定 ​

⚠️ 各分页方法别名不统一(v1.0.0 现状),按方法区分:

方法FROM 主表别名支持的关联别名
pageTodoTasks / pageDoneTaskst = wf_process_taskpi=wf_process_instance、pd=wf_process_define、pta=wf_process_task_actor
pageInstances / pageCcInstancest = wf_process_instancepd=wf_process_define、cc=wf_process_cc_instance
pageDefinest = wf_process_define(无关联表)
pageDefines 过滤列t.name / t.state / t.displayName 等(白名单见仓库实现)—

列名在仓库层过白名单校验,不在白名单的自动丢弃(v1.0.1 计划改为报错,调试更友好)。踩坑提醒:pageDefines 过滤请用 t.xxx 而非 pd.xxx(pd 仅 task 分页可用)。

参考实现:JdbcProcessRepository ​

jeeflow-repository-jdbc 模块提供纯 JDBC 实现(零 ORM 依赖),注入 DataSource 即可对接任意数据库(MySQL / H2 / PostgreSQL):

java
JdbcProcessRepository repository = new JdbcProcessRepository(dataSource);
  • 自动映射 wf_* 5 张表(spec §2),兼容 mldong 框架(boot2 版)表结构
  • 主键用 IIdGenerator SPI(不注册则内置时间戳+序号)
  • 事务(spec §7.4):引擎方法不开启事务,由业务层用 ITransactionTemplate 包装——Spring Boot 场景使用 @Transactional 或 TransactionTemplate,仓储所有方法从 DataSource.getConnection() 取连接,天然加入当前 Spring 事务

IJsonProvider(必须) ​

JSON 序列化/反序列化。引擎用此接口解析流程定义 JSON。

java
public interface IJsonProvider {
    String toJson(Object obj);
    <T> T fromJson(String json, Class<T> type);
    <T> T fromJson(String json, TypeReference<T> typeRef);
    boolean isJson(String str);
}

内置实现(Spring Boot): JacksonJsonProvider。复用 Boot 的 ObjectMapper。


IUserProvider(可选) ​

用户信息查询。不注册则流程变量中的用户名不完整(不影响核心流转)。

java
public interface IUserProvider {
    /** 一次返回用户全部信息(为空时返回 null)——避免多次查库 */
    UserInfo getUser(String userId);
}

注意:v1.0.0 起为单方法 getUser(一次返回完整 UserInfo:userId/realName/deptId/deptName/postId/postName), 早期 5 个 getRealName/getDeptId/... 方法的版本已废弃。


IOrgUserProvider(可选,v1.6.0) ​

组织维度取人——内置组织 handler(部门领导/分管领导/角色)的数据源。 业务方只实现数据接口,不写 handler(原来 boot4 的 8 个 handler 只剩 1 个数据实现)。

java
public interface IOrgUserProvider {
    /** 部门领导(deptId → 领导 userId 列表) */
    List<String> findDeptLeaders(String deptId);
    /** 部门分管领导(deptId → 分管领导 userId 列表) */
    List<String> findDeptMainLeaders(String deptId);
    /** 按角色取人(roleCode → userId 列表) */
    List<String> findByRole(String roleCode);
}
java
// 业务实现:适配组织服务
public class JeeflowOrgUserProvider implements IOrgUserProvider {
    @Override public List<String> findDeptLeaders(String deptId) { return deptApi.getLeaderIds(deptId); }
    @Override public List<String> findDeptMainLeaders(String deptId) { return deptApi.getMainLeaderIds(deptId); }
    @Override public List<String> findByRole(String roleCode) { return userApi.findByRoleCode(roleCode); }
}

引擎内置 7 个通用 handler,流程定义里 assignmentHandler 写 Java 类全限定名即可 内置 handler 的场景/配置/注意事项见 用户指南 07 · 参与者解析。


ITransactionTemplate(可选) ​

ITransactionTemplate(可选) ​

事务模板。Spring Boot 项目自动使用 TransactionTemplate。

java
public interface ITransactionTemplate {
    <T> T execute(Supplier<T> action);
}

不注册则引擎裸执行(适合内存测试)。


IExpressionEvaluator(可选) ​

表达式求值器。决策节点和会签条件需要。不注册则使用决策/会签时报错。

java
public interface IExpressionEvaluator {
    Object eval(String expression, Map<String, Object> context);
}

内置实现(Spring Boot): SpelExpressionEvaluator,基于 Spring EL。


IIdGenerator(可选) ​

ID 生成器。返回 long 类型主键。不注册则使用内置的时间戳+序列。

java
public interface IIdGenerator {
    long nextId();
}

内置实现(Spring Boot): SnowflakeIdGenerator。


管理扩展(v1.1.0,可选 SPI) ​

设计稿 / 历史 / 委托三张表由 IProcessExtRepository 提供统一读写(规范见文档站 spec §10):

java
// 参考实现:JDBC 版(给定 DataSource 即可)
IProcessExtRepository extRepo = new JdbcProcessExtRepository(dataSource);

// 委托生效查询(SurrogateInterceptor 内部使用)
ProcessSurrogate s = extRepo.getSurrogate("zhangsan", "leave", LocalDateTime.now());

委托自动生效(引擎内置、默认开启):任务落库前,引擎对每个参与者查一次生效委托 (getSurrogate),命中则把被委托人并入该任务的参与者集合随任务一起落库, 原授权人保留、任一可办(委托不是转办)。集成方零配置即生效。

java
// 关闭(回到"仅台账":配了委托也不会追加参与者)——一行
engine.configure(new Configuration().surrogateAutoApply(false));
// Spring Boot 侧等价配置项
// jeeflow.surrogate.auto-apply=false

未注册 IProcessExtRepository 时该行为静默跳过(委托是增强能力,缺仓储属于正常部署形态, 不会打断建单);JeeflowFacade 构造时传入的扩展仓储会自动暴露给引擎,无需二次注册。

SurrogateInterceptor 也是普通的 FlowInterceptor 实现,需要自定义委托策略 (如按部门改写、级联委托)时可注册自己的实例替换之,追加逻辑幂等,两者并存不会重复加人:

java
ServiceContext.put("surrogateInterceptor", new SurrogateInterceptor(extRepo));

统一门面(v1.1.0) ​

"接口即 POST + JSON body"风格——集成方只写一个转发 controller:

java
@PostMapping("/wf/**")
public Map<String, Object> flow(HttpServletRequest req, @RequestBody Map<String, Object> body) {
    String action = req.getRequestURI().substring(req.getContextPath().length() + "/wf/".length());
    return facade.flow(action, body);
}

JeeflowFacade.flow(action, map) 路由全部 action(清单以 jeeflow-doc/docs/spec/06-facade.md 为准,不在本文写死条数免漂移), 返回 {code, msg, data};deploy 自动做版本管理,execute 按 submitType 全分发。 操作人约定:args.operator 显式传入(集成方可替换为登录上下文注入)。

jeeflow · 轻量级多语言工作流引擎