ITPub博客

首页 > IT基础架构 > 服务器/存储 > 车小妹规则引擎实践

车小妹规则引擎实践

服务器/存储 作者:架构师修行手册 时间:2023-06-28 15:18:57 0 删除 编辑

来源:之家技术


1.规则引擎介绍

1.1

规则引擎定义

规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。


1.2

规则引擎和流程引擎的区别

很多人容易把规则引擎和流程引擎的概念混在一起。规则引擎主要解决易变逻辑和业务耦合的问题,规则驱动逻辑。以前项目内写死在代码里的逻辑用规则引擎可以提出来,可以支持热变更并且可读性更强。而流程引擎实现了将多个业务参与者之间按照某种预定义的规则进行流转,通常需要涉及到多角色信息。简单来说就是,流程引擎主要解决业务在不同角色之间的流转问题,如请假流程,审批流程,往往要经过多个角色。规则驱动在角色之间流转。


1.3

为什么要使用规则引擎

规则一般使用如下:

When <满足什么条件> Then <执行什么动作>


►规则引擎一般具备以下好处:


  • 声明式编程

规则引擎允许您说“该怎么做”,而不是“怎么做”。

使用规则可以使表达困难问题的解决方案变得容易,因此可以验证这些解决方案。规则比代码容易阅读。

  • 逻辑与数据分离

数据在域对象中,逻辑在规则中,这从根本上打破了数据和逻辑的耦合。解耦逻辑可以更容易维护。可以将逻辑全部组织在一个或多个非常不同的规则文件中,而不是将逻辑分布在许多域对象或控制器中。

  • 知识集中

通过使用规则,您可以创建可执行的知识库(知识库)。这意味着,规则具有很高的可读性,因此它们也可以用作文档。

  • 可理解的规则

通过创建对象模型以及领域建模的特定语言,可以编写与自然语言非常接近的规则。以自己的语言表达自己的逻辑,可以让非技术领域的专家容易理解。规则引擎主要完成的就是将业务规则从代码中分离出来。

在规则引擎中,利用规则语言将规则定义为 if-then 的形式,if 中定义了规则的条件,then 中定义了规则的结果。规则引擎会基于数据对这些规则进行计算,找出匹配的规则。这样,当规则需要修改时,无需进行代码级的修改,只需要修改对应的规则,可以有效减少代码的开发量和维护量。

 

2.规则引擎框架介绍

常用的规则引擎框架目前主要有Drools、Aviator、EasyRule、QLExpress,如下表格是对这些规则引擎的分析对比和介绍


3.规则引擎在二手车的应用实践

3.1

背景

一站式买车服务为用户提供专业安全放心的一站式服务,为商家赋能并提效,促成更快的成交转化。买车一站式,需天天拍工作人员登录车小妹对采集、服务工单进行操作。

在车小妹系统完成试驾流程,涉及确认看车,开始看车,结束看车,看车反馈等流程,基本流程一致,故引入规则引擎,抽象业务流程,简化业务实现,提高开发效率,使代码可读性和可维护性大大增强。一站式买车二期还增加了很多的流转节点,规则引擎实现主要靠配置即可,写很少量的java逻辑代码即可实现。规则引擎基于Easy Rules为原型,做了简化实现。


3.2

业务流程

编辑(修改看车时间),确认(确认看车),开始看车,结束试驾/看车, 完成(看车反馈)


3.3

技术实现

►1.加入工作流引擎模块代码

基于Easy Rules为原型,做了简化实现。技术实现简化为两部分:

1)使用反射机制实现规则流转,获取执行动作的服务。

2)使用Spring-SpEL表达式根据condition条件查找节点

 /**
 * 工作流程上下文
 *
 * @author mac
 * @date 2020/10/26
 */

public class WorkFlowContext {


    /**
     * bean提供者
     */

    private BeanProvider beanProvider;


    /**
     * 要求规范
     */

    private Object requestSpec;




    /**
     * 响应规范
     */

    private Object responseSpec;


    /**
     * 过渡的名字
     */

    private String transitionName;


    /**
     * 得到过渡的名字
     *
     * @return {@link String}
     */

    public String getTransitionName() {
        return transitionName;
    }


    /**
     * 设置过渡名称
     *
     * @param transitionName 过渡的名字
     * @return {@link WorkFlowContext}
     */

    public WorkFlowContext setTransitionName(String transitionName) {
        this.transitionName = transitionName;
        return this;
    }


    /**
     * 得到bean提供者
     *
     * @return {@link BeanProvider}
     */

    public BeanProvider getBeanProvider() {
        return beanProvider;
    }


    /**
     * 设置bean提供者
     *
     * @param beanProvider bean提供者
     * @return {@link WorkFlowContext}
     */

    public WorkFlowContext setBeanProvider(BeanProvider beanProvider) {
        this.beanProvider = beanProvider;
        return this;
    }




    /**
     * get请求规范
     *
     * @return {@link T}
     */

    public <T> T getRequestSpec() {
        return (T) requestSpec;
    }




    /**
     * 设置请求规范
     *
     * @param requestSpec 要求规范
     * @return {@link WorkFlowContext}
     */

    public WorkFlowContext setRequestSpec(Object requestSpec) {
        this.requestSpec = requestSpec;
        return this;
    }


    /**
     * 得到响应规范
     *
     * @return {@link T}
     */

    public <T> T getResponseSpec() {
        return (T) responseSpec;
    }


    /**
     * 设置响应规范
     *
     * @param responseSpec 响应规范
     * @return {@link WorkFlowContext}
     */

    public WorkFlowContext setResponseSpec(Object responseSpec) {
        this.responseSpec = responseSpec;
        return this;
    }
}


/**
 *规则引擎接口
 * @author mac
 * @date 2020/10/28
 */

public interface WorkFlowEngine {
    /**
     * 执行
     *
     * @param context 上下文
     */

    void perform(WorkFlowContext context);
    /**
     * 找到模板
     *
     * @param context 上下文
     * @return {@link WorkFlowTemplate}
     */

    WorkFlowTemplate findTemplate(WorkFlowContext context);
}
/**
 * 工作流程操作
 *
 * @author mac
 * @date 2020/10/26
 */

public interface WorkFlowAction {
    /**
     * 执行
     *
     * @param context 上下文
     */

    void execute(WorkFlowContext context);
}
/**
 * Spring的表达式检查
 *
 * @author mac
 * @date 2020/10/26
 */

public class SpringExpressionCheck implements ConditionCheck {
    /**
     * 日志记录器
     */

    static Logger LOGGER = LoggerFactory.getLogger(SpringExpressionCheck.class);
    /**
     * 配置
     */

    static SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
            SpringExpressionCheck.class.getClassLoader());
    /**
     * 解析器
     */

    static ExpressionParser parser = new SpelExpressionParser(config);
    /**
     * eval上下文
     */

    private EvaluationContext evalContext;
    /**
     * Spring的表达式检查
     *
     * @param context 上下文
     */

    public SpringExpressionCheck(WorkFlowContext context) {
        evalContext = new StandardEvaluationContext(context);
    }


    /**
     * 检查
     *
     * @param condition 条件
     * @return boolean
     */

    @Override
    public boolean check(String condition) {
        if (StringUtils.isEmpty(condition)) {
            return false;
        }
        try {
            return parser.parseExpression(condition).getValue(evalContext, Boolean.class);
        } catch (Exception e) {
            LOGGER.error("condition parse exception:" + condition, e);
            throw new WorkFlowException(WorkFlowErrorCode.CONDITION_CHECK_ERROR, condition, e.getMessage());
        }
    }
}

►2.JSON文件规则定义

name为节点名称,condition为执行的条件,transitions为过渡节点定义,定义当前节点通过action动作执行到目标to节点上。desc为节点描述,可以使用中文描述,可读性更强。

以【顾问确认接单】节点为例,如果执行的动作是【确认看车】,那么就流转到【顾问确认看车】节点

buyCar.json
{
 "name": "买车流程工作流模板",
 "flow_id": "buyCar",
 "init_data": {
   "status": 1,
   "action": 1
 },
 "nodes": [
   {
     "name": "payedNode",
     "desc": "已支付节点",
     "start": true,
     "condition": "status == 101",
     "transitions": [
       {
         "name": "CONFIRM_ORDER_ACTION",
         "to": "editorTakeOrderNode",
         "action": "confirmOrderAction"
       },
       {
         "name": "UPDATE_SEE_CAR_TIME_ACTION",
         "to": "payedNode",
         "action": "updateSeeCarTimeAction"
       }
     ]
   },
   {
     "name": "editorTakeOrderNode",
     "desc": "顾问确认接单节点",
     "condition": "status == 130",
     "transitions": [
       {
         "name": "CONFIRM_CAR_ACTION",
         "to": "editorTakeCarNode",
         "action": "confirmCarAction"
       },
       {
         "name": "UPDATE_SEE_CAR_TIME_ACTION",
         "to": "editorTakeOrderNode",
         "action": "updateSeeCarTimeAction"
       }
     ]
   },
   {
     "name": "editorTakeCarNode",
     "desc": "顾问确认看车节点",
     "condition": "status == 135",
     "transitions": [
       {
         "name": "CHECK_CAR_ACTION",
         "to": "editorTakeCarOverNode",
         "action": "checkCarAction"
       },
       {
         "name": "UPDATE_SEE_CAR_TIME_ACTION",
         "to": "editorTakeCarNode",
         "action": "updateSeeCarTimeAction"
       }
     ]
   },
   {
     "name": "editorTakeCarOverNode",
     "desc": "看车结束节点",
     "condition": "status == 136",
     "transitions": [
       {
         "name": "CHECK_CAR_ACTION",
         "to": "editorTakeCarOverNode",
         "action": "checkCarAction"
       }
     ]
   }
 ]
}


►3. 动作的基类实现流程编排

包含参数校验,参数构建,存储状态变更和动作日志。


/**
 * 买车一站式流程编排
 *
 * @author chaizx
 * @date 2022/11/09
 */

@Slf4j
@Component
public class BaseBuyCarAction implements WorkFlowAction {
    @Resource
    ConsignmentCarOrderRecordMapper consignmentCarOrderRecordMapper;
    @Resource
    ConsignmentCarOrderManager consignmentCarOrderManager;


    @Override
    public void execute(WorkFlowContext context) {
        CxmActionOperationVo request = context.getRequestSpec();
        ConsignmentCarOrderRecord record = this.buildParam(request);
        Result resultCheck = this.checkParam(request,record);
        if(resultCheck.isFailure()){
            context.setResponseSpec(resultCheck);
            return;
        }


        this.buildParamAfter(request,record);
        log.info("【买车一站式动作执行】,参数构建完成 record:{}",record);
        try {
            consignmentCarOrderManager.saveStatusAndLog(record);
            context.setResponseSpec(Result.createSuccessResult());
        }catch (Exception e){
            context.setResponseSpec(Result.createFailureResult(CxmExceptionEnum.SYSTEM_ERROR));
            log.error("动作执行失败 requestSpec:{}",request,e);
        }
    }


    public Result checkParam(CxmActionOperationVo request,ConsignmentCarOrderRecord  record) {
        if (request == null){
            return Result.createFailureResult(CxmExceptionEnum.PARAM_ILLEGAL);
        }
        if(record == null){
            return Result.createFailureResult(CxmExceptionEnum.RECORD_NOT_EXISTS);
        }
        //状态变更,和库不一样
        if(!record.getStatus().equals(request.getStatus())){
            return Result.createFailureResult(CxmExceptionEnum.STATUS_CHANGED);
        }
        //不是订单所属顾问,不能操作
        if (!request.getOptid().equals(record.getEditid())){
            return Result.createFailureResult("参数错误,不是订单所属顾问,不能操作",CxmExceptionEnum.PARAM_ILLEGAL.getCode());
        }
        return Result.createSuccessResult();
    }


    /**
     * 组装修改参数
     * @return 参数
     */

    public ConsignmentCarOrderRecord buildParam(CxmActionOperationVo request) {
        if(request == null){
            return null;
        }
        ConsignmentCarOrderRecord  record =consignmentCarOrderRecordMapper.selectByPrimaryKey(request.getCcorid());
        return record;
    }


    public void buildParamAfter(CxmActionOperationVo request,ConsignmentCarOrderRecord record) {
    }


}


►4. 具体的动作只处理参数校验和状态变更逻辑

/**
 * 确认看车
 *
 * @author chaizx
 * @date 2022/11/09
 */

@Slf4j
@Component("confirmCarAction")
public class ConfirmCarAction extends BaseBuyCarAction{
    @Override
    public Result checkParam(CxmActionOperationVo request, ConsignmentCarOrderRecord record) {
        Result resultSupper = super.checkParam(request,record);
        if(resultSupper.isFailure()){
            return resultSupper;
        }
        //不是顾问接单状态
        if (!CarOrderRecordStatusEnum.TakeOrder.getValue().equals(record.getStatus())){
            return Result.createFailureResult("参数错误,不是顾问接单状态",CxmExceptionEnum.PARAM_ILLEGAL.getCode());
        }
        return Result.createSuccessResult();
    }
    @Override
    public void buildParamAfter(CxmActionOperationVo request,ConsignmentCarOrderRecord record) {
        record.setStatus(CarOrderRecordStatusEnum.TakeCar.getValue());
        ConsignmentCarOrderRecordLog recordLog = ConsignmentCarOrderRecordLog.builder()
                .ccorid(record.getCcorid())
                .infoid(record.getInfoid())
                .logtype(record.getStatus())
                .remark(SafesUtil.of(request.getRemark(),CarOrderRecordStatusEnum.getNameByValue(record.getStatus())))
                .optid(request.getOptid())
                .optname(MobileUtils.MobileEncrypt(request.getOptname())).build();
        record.setRecordLogList(Arrays.asList(recordLog));
    }
}


►5. 前端http接口调用传动作类型,规则引擎驱动动作执行

@ApiOperation("确认接单")
@PostMapping("confirmOrder")
public Result confirmOrderAction(@Valid CxmActionOperationVo actionOperationVo) {
    fillUserInfo(actionOperationVo);
    actionOperationVo.setActionType(CxmBuyCarActionEnum.CONFIRM_ORDER_ACTION.getCode());
    Result result = actionExecuteService.actionExecute(actionOperationVo);
    return result;
}


4.核心价值

4.1

实现知识集中以及规则的可读性更强

通过JSON解析配置文件,实现【声明式编程】,哪个节点执行什么动作流转到什么节点,使逻辑和数据分离,流转逻辑规则集中在配置文件中,通过节点的中文描述可以更快的理解规则。比硬编码可读性更强,硬编码在条件多或状态多的情况下,很难靠大脑记住,查找代码逻辑比在配置文件中定位要慢。


4.2

更好的业务扩展性

后续增加状态,服务编排逻辑不需要处理,只需要增加配置节点,动作的核心逻辑点调整即可。代码层次清晰,好维护,好扩展。比硬编码好扩展。


4.3

引入规则引擎

卖车等其他业务也可以使用

业务场景比较复杂的场景,可以考虑使用规则引擎,通过配置化使规则理解更简单,扩展更方便。应用的场景很多可以用规则引擎。


4.4

自测的成本降低50%

流程编排是不变的,变的是具体的动作,只测具体的状态变更逻辑即可。测试过程不会出现流程遗漏的情况。减少代码测试工作量大概50%。


4.5

自测的成本降低50%

流程编排是不变的,变的是具体的动作,只测具体的状态变更逻辑即可。测试过程不会出现流程遗漏的情况。减少代码测试工作量大概50%。


4.6

 可以支持热更新

不需要频繁发布,效率更高

规则配置或脚本可以存放在文件或特定的存储介质中(例如存放在数据库中) ,使得业务规则的变更不需要修改项目代码、不用重启服务器就可以在线上环境立即生效。


4.7

代码结构统一,利于迭代维护

流程编排统一,代码结构清晰,团队编码风格容易保持一致,符合团队对敏捷或迭代开发过程的要求。


5.总结

通过引入规则引擎,可以将复杂的业务规则分离出来,降低代码的耦合度,提高代码的可维护性。


5.1

从开发人员视角来看

 在没有规则引擎的时代,有些逻辑比较复杂的业务,只有不断的增添if-else去满足我们这个复杂的业务场景,对于开发者来说还好,对于后面接手的同学一看到处都是if-else,体验过的同学就会知道,当然if-else可以通过一些模式去优化,比如使用策略模式,或者使用一些注解进行扩展点优化,这样的确可以解决一部分代码不清晰的问题,但是依然无法解决开发缓慢,需要上线等问题。举个例子,在风控系统中,因为风控的逻辑在不断的发生一个改变,如果我们在代码中去写死,那么发生一个改变就改一下代码,上一下线,这明显是我们不能接受的。所以我们需要规则引擎去改变这个现状,通过高效可靠的方式去做这些业务规则的改变。

 

5.2

从业务人员视角来看

以前的开发模式是业务人员提出业务规则叫开发人员做出相对应的业务开发,到底这个最后开发出来的业务规则是否和业务人员所提出来的是否一致,需要通过大量的测试去进行验证。而我们的开发人员理解业务很容易和业务人员的提出的业务有偏差,就会导致开发成本上升。

有了规则引擎之后,我们就可以有下面几点提升:

1) 业务人员独立配置业务规则,开发人员无需理解,让业务人员的规则和真正的实际情况一致。

2) 增加业务的透明程度,业务人员配置了之后其他业务人员也能够知道,以前只能通过代码口口相传。

3) 规则高效改动和上线,一般业务人员提出需求之后都是希望能尽快上线,但是之前都需要有代码开发,项目上线等环节,现在业务人员配置好了之后即配即用。

4) 减少业务人员和开发人员的矛盾,开发人员通常会因为一些时间因素或者一些理解不到位导致业务人员的规则实现有偏差,最后业务同学会对开发同学产生一些小小的矛盾,这下完全业务配置解除开了之后,只要不断的升级规则引擎,业务规则就不会再对开发人员有依赖。

 

作者简介


柴志学

■ 二手车事业部-技术部-专项支持团队

■本人2022年10月入职汽车之家二手车事业部,多年从事零售供应链的核心系统研发,对处理复杂业务简单化,以及对高并发和高可用系统优化,有过很深入的实战经验。欢迎同学们一起沟通交流。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027824/viewspace-2961239/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2023-03-16

  • 博文量
    358
  • 访问量
    290085