暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

基于DSL规则引擎落地方案(附源码)

匠工精神 2021-08-28
4747

一、背景

在之前的一篇业务规则引擎初试 文章中尝试了使用业务规则引擎来处理促销模块的功能实现,条件其实还相对单一,随着业务的迭代,会有复杂的组合条件的规则匹配,对这一块的功能通过DSL和前端拖拽的控件处理把我们赠品服务重构了一下,接下来就带大家分享下实践方案;


一段代码越容易看懂,就越容易发现错误,也就越容易对系统进行修改.


二、DSL 初识

在Martin Fowler的《Domain Specific Languages》书中。开篇就是以State Machine来作为引子介绍DSL的。有时间的话,强烈建议你去读读这本书。

DSL是一种工具,它的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。

按照定义来说,DSL是针对某一特定领域,具有受限表达性的一种计算机程序设计语言。这一定义包含3个关键元素:

  • 语言性(language nature):DSL是一种程序设计语言,因此它必须具备连贯的表达能力——不管是一个表达式还是多个表达式组合在一起。


  • 受限的表达性(limited expressiveness):通用程序设计语言提供广泛的能力:支持各种数据、控制,以及抽象结构。这些能力很有用,但也会让语言难于学习和使用。DSL只支持特定领域所需要特性的最小集。使用DSL,无法构建一个完整的系统,相反,却可以解决系统某一方面的问题。


  • 针对领域(domain focus):只有在一个明确的小领域下,这种能力有限的语言才会有用。这个领域才使得这种语言值得使用。

总结一下,DSL是什么?全称是Domain Specific Language,领域特定语言。举个例子SQL就是数据库领域的交互语言,它定义了一套标准语法,各大数据库厂商(如mysql、oracle)对其进行解析实现,任何人都可通过编写SQL实现与数据库的交互。类似还有正则表达式、HTML&CSS等均形成了自己的语法标准。


三、实践

为了完成促销规则引擎,我们也实现一套自定义DSL语法,并对其解析,通过DSL语法实现与规则引擎的交互,一条规则的执行过程大致如下:先通过数据计算出特征(理解入参),然后带入条件表达式计算,并对多个表达式结果做逻辑运算,最终根据逻辑运算决定是否触发结果。实际的业务需求通常不会只有一组规则,会对不同的规则、规则集进行编排,还会出现分支流程,子流程,形成一个更复杂决策流程,我们叫决策流,决策流的解析就是实现一个流程引擎。

此处为了演示方便,使用yml文件定义规则:

workflow:
- node:
nodeName: start_test
category: start
nextNodeName: conditional_1
nextCategory: conditional
- node:
nodeName: conditional_1
category: conditional
nextNodeName: ""
nextCategory: ""
- node:
nodeName: ruleset_1
category: ruleset
nextNodeName: end_1
nextCategory: end
- node:
nodeName: ruleset_2
category: ruleset
nextNodeName: end_2
nextCategory: end
- node:
nodeName: end_1
category: end
nextNodeName: ""
nextCategory: ""
- node:
nodeName: end_2
category: end
nextNodeName: ""
nextCategory: ""
ruleSets:
- ruleset:
rulesetName: ruleset_1
rulesetCategory: internal
ruleExec: allrun
depends: [feature_1,feature_2,feature_3]
rules:
- rule:
ruleName: rule_1
conditions:
- condition:
feature: feature_1
operator: GT
value: 50
logic: ""
decision: reject
- rule:
ruleName: rule_2
conditions:
- condition:
feature: feature_2
operator: LT
value: 18
- condition:
feature: feature_3
operator: GT
value: 50
logic: OR
decision: reject
- ruleset:
rulesetName: ruleset_2
rulesetCategory: internal
ruleExec: allrun #rule execution strategy:allrun,rejectbreak
depends: [feature_1,feature_2]
rules:
- rule:
ruleName: rule_3
conditions:
- condition:
feature: feature_1
operator: GT
value: 50
logic: ""
decision: reject
- rule:
ruleName: rule_4
conditions:
- condition:
feature: feature_2
operator: LT
value: 18
- condition:
feature: feature_2
operator: GT
value: 50
logic: OR
decision: reject
conditionals:
- conditional:
conditionalName: conditional_1
depends: [feature_4]
branches:
- branch:
branchName: branch_1
conditions:
- condition:
feature: feature_4
operator: GT
value: 5
- condition:
feature: feature_4
operator: LE
value: 10
logic: AND
decision: ruleset_1
- branch:
branchName: branch_2
conditions:
- condition:
feature: feature_4
operator: GT
value: 10
logic: ""
decision: ruleset_2


对上面的结构做下说明,workflow部分,可以理解为一个业务处理的流程,ruleSets部分,一系列的规则集合,conditionals部分,理解为条件网关的实现。

条件表达式计算完后会有一个结果,然后通过逻辑运算计算得到最终的结果:

这里会涉及到诸如 <=、>=、<、>等条件的判断处理及逻辑运算 AND、OR等组合的计算,系统引用的是groovyshell,在java里处理起来也很方便

/**
* 条件表达式判断是否为true
* @param left
* @param right
* @param operator
* @return
*/
public static boolean evaluate(Object left,String operator,Object right) {
Binding binding = new Binding();
binding.setVariable("left", left);
binding.setVariable("right", right);
String expression = "left %s right";
String operation = OperatorMap.valueOf(operator).getCode();
expression = String.format(expression, operation);
GroovyShell groovyShell = new GroovyShell(binding);
String result = groovyShell.evaluate(expression).toString();
return Boolean.parseBoolean(result);
}


/**
* 逻辑表达式的计算
* @param results
* @param logic
* @return
*/
public static boolean logical(List<Boolean> results,String logic) {
if(!CollectionUtils.isEmpty(results)) {
if (results.size() == 1) {
return results.get(0);
}

String expression = "" ;
for(int i=0; i< results.size();i++) {
expression += results.get(i);
if (i != results.size()-1) {
expression += LogicMap.valueOf(logic).getCode();
}
}

GroovyShell groovyShell = new GroovyShell();
String result = groovyShell.evaluate(expression).toString();
return Boolean.parseBoolean(result);

}
return false;

}


条件判断处理抽象:

/**
* @author villiam
* @date 2021-08-21
*
* 操作符号抽象
*/
public enum OperatorMap {
GT("GT",">"),
LT("LT","<"),
GE("GE",">="),
LE("LE","<="),
EQ("EQ","=="),
NEQ("NEQ","!=");
private String name;
private String code;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
OperatorMap(String name, String code){
this.code = code;
this.name = name;
}

}


/**
* @author villiam
* @date 2021-08-21
*
* 逻辑运算符抽象处理
*/
public enum LogicMap {
AND("AND","&&"),
OR("OR","||");
private String name;
private String code;
LogicMap(String name,String code){
this.code = code;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}

yml文件的解析用的是spring的 YamlMapFactoryBean:


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yilimaizi.decision.dsl.Dsl;
import org.springframework.beans.factory.config.YamlMapFactoryBean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.util.Map;

public class YmlRegister {

private static final String CLASSPATH_FLOW="*.yml";


public static Dsl init() {
try {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(CLASSPATH_FLOW);

for(Resource resource: resources){
YamlMapFactoryBean yamlMapFactoryBean = new YamlMapFactoryBean();
yamlMapFactoryBean.setResources(resource);
yamlMapFactoryBean.afterPropertiesSet();
Map<String, Object> object = yamlMapFactoryBean.getObject();
System.out.println("parse result is =="+JSON.toJSONString(object));
Dsl flow = JSONObject.parseObject(JSON.toJSONString(object), Dsl.class);
return flow;
}

}catch (Exception e){
e.printStackTrace();
}
return null;
}

}


最后再附上domain的类:

/**
* @author villiam.li
* @date 2021-08-21
*
* workflow 节点
*/
@Data
public class Node {
private String nodeName;

private String category;

private String nextNodeName;

private String nextCategory;
}



/**
* @author villiam.li
* @date 2021-08-21
*
* 规则抽象
*/
@Data
public class Rule {

private List<Condition> conditions;

private String ruleName;

private String ruleGroup;

private String logic;

private DecisionMap decision;

private List<String> depends;

public boolean parse(Map<String, Feature> depends,Rule rule) {
List<Boolean> conditionResults = new ArrayList<>();
if (Objects.nonNull(rule)){
if(!CollectionUtils.isEmpty(rule.getConditions())) {
for(Condition condition: rule.getConditions()) {
if(depends.containsKey(condition.getFeature())) {
if (StringUtils.isEmpty(depends.get(condition.getFeature()).getName())) {
continue;
}

boolean result = GroovyExpressionUtil.evaluate(depends.get(condition.getFeature()).getValue(),condition.getOperator(),condition.getValue());
conditionResults.add(result);
}
}
}
return GroovyExpressionUtil.logical(conditionResults,rule.getLogic());

}
return false;

}

}



/**
* @author villiam.li
* @date 2021-08-21
*
* 规则集的抽象模型
*/
@Data
public class Ruleset {

private String ruleSetName;

private String ruleSetCategory;

private String ruleExec;

private List<Rule> rules;

private List<String> depends;


public Object parse(Ruleset ruleset, DslResult result, GlobalFeatures globalFeatures) {
NodeResult nodeResult = new NodeResult();
nodeResult.setNodeName(ruleset.getRuleSetName());

Map<String, Feature> depends = globalFeatures.get(ruleset.getDepends(),globalFeatures);
nodeResult.setFactor(depends);

List<Object> hits = new ArrayList<>();
List<Integer> ruleResult = new ArrayList<>();

for(Rule rule: ruleset.getRules()) {
boolean flag = rule.parse(depends,rule);
Integer ruleDecision = DecisionMap.REJECT.getCode();
if (flag) {
hits.add(rule.getRuleName());
nodeResult.setHits(hits);
ruleDecision = rule.getDecision().getCode();
}
ruleResult.add(ruleDecision);
}

if(!CollectionUtils.isEmpty(ruleResult)) {
nodeResult.setDecision(ruleResult.get(0));
result.getDetail().add(nodeResult);
return ruleResult.get(0);
}

return null;
}
}



/**
* @author villiam.li
* @date 2021-08-21
*
* dsl抽象
*/
@Data
public class Dsl {

private List<Node> workflow;

private List<Ruleset> ruleSets;

private List<Conditional> conditionals;


/**
* 获取第一个节点
* @param dsl
* @return
*/
public Node findStartNode(Dsl dsl){
if (Objects.nonNull(dsl)) {
if (!CollectionUtils.isEmpty(dsl.getWorkflow())) {
for(Node node: dsl.getWorkflow()) {
if(Constants.START.equals(node.getCategory())) {
return node;
}
}
}
}
return null;
}


/**
* 根据名称获取节点
* @param dsl
* @param nodeName
* @return
*/
public Node findNode(Dsl dsl,String nodeName){
if (Objects.nonNull(dsl)) {
if (!CollectionUtils.isEmpty(dsl.getWorkflow())) {
for(Node node: dsl.getWorkflow()) {
if(nodeName.equals(node.getNodeName())) {
return node;
}
}
}
}
return null;
}


/**
* 根据名称获取ruleset
* @param dsl
* @param name
* @return
*/
public Ruleset findRuleset(Dsl dsl,String name){
if (Objects.nonNull(dsl)) {
if (!CollectionUtils.isEmpty(dsl.getWorkflow())) {
for(Ruleset ruleset: dsl.getRuleSets()) {
if(name.equals(ruleset.getRuleSetName())) {
return ruleset;
}
}
}
}
return null;
}


public Conditional findConditional(String name,Dsl dsl) {
for(Conditional conditional: dsl.getConditionals()){
if (conditional.getConditionalName().equals(name)) {
return conditional;
}
}
return null;
}

/**
* 获取下一个node
* @param dsl
* @param nodeName
* @param category
* @param result
*/
public void gotoNextNode(Dsl dsl, String nodeName, String category, DslResult result, GlobalFeatures globalFeatures) {

Node node = dsl.findNode(dsl,nodeName);
List<String> trackList = new ArrayList<>();
if (Objects.nonNull(node)) {
trackList.add(nodeName);
result.setNextNodeName(node.getNextNodeName());
result.setNextCategory(node.getNextCategory());
if (CollectionUtils.isEmpty(result.getTrack())) {
result.setTrack(trackList);
}else {
result.getTrack().addAll(trackList);
}


switch (category) {
case Constants.START:
return;
case Constants.RULESET:
Ruleset ruleset = dsl.findRuleset(dsl,node.getNodeName());
Object decision = ruleset.parse(ruleset,result,globalFeatures);
if(Objects.nonNull(decision)) {
result.setDecision(decision);
}
return;
case Constants.CONDITIONAL:
Conditional conditional = findConditional(node.getNodeName(),dsl);
Object object = conditional.parse(conditional,result,globalFeatures);
if (Objects.nonNull(object)) {
result.setNextNodeName(object.toString());
result.setNextCategory(dsl.findNode(dsl,object.toString()).getCategory());
}
return;
case Constants.END:
result.setNextCategory(null);
result.setNextNodeName(null);

return;
default:
return;

}
}
}

public boolean isBreakDecision(Object decision){
if (Objects.isNull(decision)) {
return false;
}
return decision.toString().equals(DecisionMap.REJECT.getCode().toString());
}
}



/**
* @author villiam.li
* @date 2021-08-21
*
* 规则网关抽象
*/
@Data
public class Conditional {
private String conditionalName;

private List<String> depends;

private List<Branch> branches;

public Object parse(Conditional conditional, DslResult result, GlobalFeatures globalFeatures){
NodeResult nodeResult = new NodeResult();
nodeResult.setNodeName(conditional.getConditionalName());

Map<String, Feature> depends = globalFeatures.get(conditional.getDepends(),globalFeatures);
nodeResult.setFactor(depends);

for(Branch branch: conditional.getBranches()){
List<Boolean> conditionResults = new ArrayList<>();
for(Condition condition: branch.getConditions()) {
if(depends.containsKey(condition.getFeature())) {
if (StringUtils.isEmpty(depends.get(condition.getFeature()).getName())) {
continue;
}

boolean rs = GroovyExpressionUtil.evaluate(depends.get(condition.getFeature()).getValue(),condition.getOperator(),condition.getValue());
conditionResults.add(rs);
}
}
Boolean flag = GroovyExpressionUtil.logical(conditionResults,branch.getLogic());

if (flag) {
nodeResult.setDecision(branch.getDecision());
if (CollectionUtils.isEmpty(result.getDetail())) {
List<NodeResult> nodeResults = new ArrayList<>();
nodeResults.add(nodeResult);
result.setDetail(nodeResults);
}else {
result.getDetail().add(nodeResult);
}

return branch.getDecision();
}

}

return null;

}
}


/**
* @author villiam.li
* @date 2021-08-21
*
* 条件判断抽象
*/
@Data
public class Condition {

private String feature;

private String operator;

private String result;

private Object value;
}



/**
* @author villiam.li
* @date 2021-08-21
*
* 条件分支抽象
*/
@Data
public class Branch {

private String branchName;

private List<Condition> conditions;

private String logic;

private Double percent;

private Object decision;
}


请求request:


/**
* @author villiam.li
* @date 2021-08-21
*
* 请求request
*/
@Data
public class DslRunRequest {

private String flow;

private Map<String, Integer> features;
}


返回结果:

/**
* @author villiam.li
*/
@Data
public class DslResult {

private String nextNodeName;

private String nextCategory;

private Object decision;

private List<String> track;

private List<NodeResult> detail;
}

最后进行测试:

@RestController
@RequestMapping("/api/v1")
public class TestController {

@PostMapping(value = "/test")
public DslResult parse(@RequestBody DslRunRequest request) {
DslResult result = new DslResult();
GlobalFeatures globalFeatures = new GlobalFeatures();
Map<String, Feature> featureMap = new HashMap<>();
Map<String, Integer> features = request.getFeatures();
features.forEach((key,value)->{
Feature feature = new Feature();
feature.setName(key);
feature.setValue(value);
featureMap.put(key,feature);
globalFeatures.setFeatures(featureMap);
});

//1. 获取资源文件
Dsl dsl = YmlRegister.init();
if (Objects.nonNull(dsl)) {
if(! CollectionUtils.isEmpty(dsl.getWorkflow())) {
Node firstNode = dsl.findStartNode(dsl);
dsl.gotoNextNode(dsl,firstNode.getNodeName(),firstNode.getCategory(),result,globalFeatures);

while (!StringUtils.isEmpty(result.getNextNodeName()) && !dsl.isBreakDecision(result.getDecision())) {
dsl.gotoNextNode(dsl,result.getNextNodeName(), result.getNextCategory(), result,globalFeatures);

}
}
}

return result;
}
}


请求示例:

curl -XPOST  -v  http://localhost:8080/api/v1/test -H'Content-Type:application/json' -d'{"flow":"flow_conditional","features":{"feature_1":18,"feature_2":30,"feature_3":20,"feature_4":30}}'; 



四、总结


配置化的规则替代了硬编码的校验逻辑,减少了修改规则发布代码维护的成本,同时也让业务逻辑与代码分离。结合目前我们在做的低代码平台,通过前端页面的拖拽组件,配置好业务流程,让业务更快的迭代。

文章转载自匠工精神,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论