背景是有一些简单的审批流程处理,不想用到工作流那么重的组件。但是,全凭if else 修改很费体力,也容易出错。需求是可以预见的在下一期或者下下期会调整审批流程。利用表达式语言来实现一个简单的流程选择。这个思路感觉比较通用,可以很方便扩展其他的类似的场景应用。主要点如下:
- 挑选一个表达式引擎
- 定义表达式与输出参数配置
- 现实配置的定义加载
- 使用表达式进行输入参数到输出参数的转化
表达式语言的组件有很多,比如ognl,velocity等等。SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。选用SpEL主要是Spring框架自带了,基本够用,尽可能少的减少项目对三方组件的依赖。比如常用到的配置参数注入注解@Value就可以使用SpEL:
// #{}内就是表达式的内容
@Value("#{表达式}")
public String arg;
简化一下需求场景,申请人任务提交后,根据不同的任务级别,不同的提报部门,选择不同的部门岗位的人员来进行审批(审批的结果流程省略了)。审批的多级支持省略,可自行扩展。尽可能完整给出示例。
其实就是需要审批权限的用户看到自己可以操作的任务,拼接查询条件去查询任务表。常规的思路就是写一堆ifelse进行判断和条件拼接。
第一先把判断条件的表达式和最后输出的条件参数定义下,为了方便操作和修改,使用了json文件,这样反序列化加载非常容易就可以得到Java的对象;定义一个flow.json,放到resources下,会编译到类加载根路径之下。
{
"approvePolicies": [
{
"expr": "dept == 'D1' and pos == 'P3'",
"data": {"level": "L1"}
},
{
"expr": "dept == 'D1' and pos == 'P1'",
"data": {"level": "L2", "src": "S1"}
},
{
"expr": "dept == 'D1' and pos == 'P1'",
"data": {"level": "L2", "src": "S1"}
}
]
}
对应的流程定义对象为:
@Data
public class ApproveCondition {
// 任务级别
String level;
// 任务来源
String src;
}
@Data
public class ApprovePolicy {
String expr;
ApproveCondition data;
}
@Data
public class FlowCfgData {
List<ApprovePolicy> approvePolicies;
}
流程加载可以放到应用容器bean初始化时,当然可以扩展一下实现动态加载(比如放到数据库),就可以随时修改和刷新流程定义了。
@Configuration
@Slf4j
public class FLowConfig {
@Bean
FlowCfgData flowCfgData() throws IOException {
return getConfigDataFromResource();
}
/**
* 从类路径下加载流程配置定义
*/
private static FlowCfgData loadCfgDataFromResource() throws IOException {
InputStream in = FLowConfig.class.getResourceAsStream("/flow.json");
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
in.close();
String info = sb.toString();
log.info("load flow config from resource: {}", info);
FlowCfgData cfgData = JSON.parseObject(info, FlowCfgData.class);
return cfgData;
}
}
入参就是当前登录用户的岗位信息:
@Data
public class UserPosition {
// 部门
private String dept;
// 岗位
private String pos;
// ...
}
那边在判断用户岗位入参,生成审批查询条件就很简单了
@Service
public class XxxServiceImpl implements XxxService{
@Autowired
FlowCfgData flowCfgData;
// ...
/**
* 把条件参数拼装为查询条件(这里看数据库和ORM选择的组件)
* 这里示例拼接SQL条件,拼接完类似: (level='L2' and src='S1') or (leve='L1')
*/
private String getQueryCondtion(List<ApproveCondition> conditionList) {
String where = "";
for(ApproveCondition condition : conditionList) {
String p = "";
if(condition.getLevel() != null){
p += "level='" + condition.getLevel() + "'";
}
if(condtion.getSrc() != null) {
p += (p.isEmpty() ? "" : " and ") + "src='" + condition.getSrc() + "'";
}
if(!p.isEmpty()) {
where += (where.isEmpty() ? "" : " or ") + "(" + p + ")";
}
}
}
/**
* 把权限转化为条件参数
*/
private List<ApproveCondition> getApproveCondition(UserPosition userPos) {
List<ApproveCondition> conditionList = new ArrayList<>();
for (ApprovePolicy policy : flowCfgData.getApprovePolicies()) {
// 满足某个表达式时,就得到改表达式对应的条件参数
if (execExpr(policy.getExpr(), userPos)) {
conditionList.add(policy.getData());
}
}
return conditionList;
}
/**
* 执行SpEL表达式
*/
private boolean execExpr(String expr, Object object) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext ctx = new StandardEvaluationContext(object);
Expression exp = parser.parseExpression(expr);
Object ret = exp.getValue(ctx);
return ret instanceof Boolean && (boolean) ret;
}
}
这样,一个完整的流程定义和加载处理的引擎就完成了。当然,选用其他表达式引擎或者选择编译型实现DSL可以提升执行效率。比如规则引擎Drools及MVEL或是Antrl,可以让代码随时修改随时运行,对于流程或规则决策型应用应用广泛。