支付路由系统设计三:命中-2

技术栈:Java+Groovy+Lua+Springboot+Mysql+Redis+Drools+Velocity+RabbitMQ+Spring Data Jpa


一、前言

上篇我们对基于交易类型维度规则的命中设计,进行了简要分析,分析了常规设计下的问题,以及提出了纵向扩展的方式。where条件不仅仅是简单的“=”了,更强大的条件判断规则,如“=、!=、in、!in、>、<、>=、<=”等,如果基本运算满足不了特殊需求场景,则切入Groovy脚本进行数据计算处理。本篇我们将完成规则表(router_rule)和条件表(rule_condition)的关联设计,即满足这条规则对应的所有条件时为命中。
本篇假设读者已经了解规则引擎Drools和模板引擎Velocity基本使用了


二、分析设计

1. Drools简介

规则引擎应用场景:

  • 风险控制系统:风险贷款、风险评估
  • 反欺诈项目:银行贷款、征信验证
  • 决策平台系统:财务计算
  • 促销平台系统:满减、打折

drools官网地址:https://drools.org/
drl文件样例:

package book.discount
import com.kkk.drools.entity.Order

//规则一:所购图书总价在100元以下的没有优惠
rule "book_discount_1"
    when
        $order:Order(originalPrice < 100)
    then
        $order.setRealPrice($order.getOriginalPrice());
        System.out.println("成功匹配到规则一:所购图书总价在100元以下的没有优惠");
end

//规则二:所购图书总价在100到200元的优惠20元
rule "book_discount_2"
    when
        $order:Order(originalPrice < 200 && originalPrice >= 100)
    then
        $order.setRealPrice($order.getOriginalPrice() - 20);
        System.out.println("成功匹配到规则二:所购图书总价在100到200元的优惠20元");
end

规则体语法结构如下:

rule "ruleName"
    when
        LHS
    then
        RHS
end

所以我们要做的就是将我们的条件表(rule_condition)中的数据转换到 LHS 部分,将规则(router_rule)转换到 RHS 部分,如何转换我们通过模板引擎进行定制模板生成。当然你也可以硬编码写,但是合适的技术解决特定的业务场景,简单明了,可维护性、可迭代性也会大大提升。

2. Velocity简介

模板引擎应用场景:

  • Web应用程序 : 作为为应用程序的视图, 展示数据。
  • 源代码生成 : Velocity可用于基于模板生成Java源代码
  • 自动电子邮件 : 网站注册 , 认证等的电子邮件模板
  • 网页静态化 : 基于velocity模板 , 生成静态网页

Velocity官网地址:https://velocity.apache.org/

1"#"用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#iinclude、#parse、#macro等;
如:
#if($info.imgs)
<img src="$info.imgs" border=0>
#else
<img src="noPhoto.jpg">
#end

2"$"用来标识一个对象(或理解为变量);如
如:$i、$msg、$TagUtil.options(...)等。

3"{}"用来明确标识Velocity变量;
比如在页面中,页面中有一个$someonename,此时,Velocity将把someonename作为变量名,若我们程序是想在someone这 个变量的后面紧接着显示name字符,则上面的标签应该改成${someone}name。

4"!"用来强制把不存在的变量显示为空白。
如当页面中包含$msg,如果msg对象有值,将显示msg的值,如果不存在msg对象同,则在页面中将显示$msg字符。这是我们不希望的,为了把不存 在的变量或变量值为null的对象显示为空白,则只需要在变量名前加一个“!”号即可。
如:$!msg

3. 模板定义

1、完整规则文件模板:

package com.kyn.router
import java.lang.*;
import java.util.*;
import com.kyn.router.service.runtime.tools.*;
import com.kyn.router.domain.entity.*;
global com.kyn.router.service.runtime.tools.ScritpExecuteTool scritpExecuteTool;
global com.kyn.router.service.runtime.tools.EventPropertyObtainTool eventPropertyObtainTool;
global com.kyn.router.service.runtime.tools.AnalyzeResultProcessTool analyzeResultProcessTool;
#foreach($ruleEntity in $ruleEntities)
    #if(!$ruleEntity.getRuleConditionEntities().isEmpty())
        $ruleTemplateGenerator.generateRuleTemplate($ruleEntity)
    #end
#end

2、单条规则模板:

rule "$ruleEntity.getId()"
when
#foreach($factExpression in $ruleEntity.getFactExpressions())
	$!factExpression
#end
#foreach($conditon in $ruleEntity.getRuleConditionEntities())
	$!conditon.getDroolsExpression()
#end
then
	$ruleEntity.getThenExpression()
end

3、最终生成的drl文件:

import java.lang.*;
import java.util.*;
import com.kyn.router.service.runtime.tools.*;
import com.kyn.router.domain.entity.*;

global com.kyn.router.service.runtime.tools.ScritpExecuteTool scritpExecuteTool;
global com.kyn.router.service.runtime.tools.EventPropertyObtainTool eventPropertyObtainTool;
global com.kyn.router.service.runtime.tools.AnalyzeResultProcessTool analyzeResultProcessTool;

rule "1"
	when
		$event : RuleCenterEvent();
		eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, "productCode"),"=","test02"));
		eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, "transCode"),"=","depute"));
	then
		analyzeResultProcessTool.processAnalyzeResult($event, "1");
	end

rule "2"
	when
		$event : RuleCenterEvent();
		eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, "transCode"),"=","depute"));
		eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, "productCode"),"=","test01"));
		eval(RuleCompareTool.compare(scritpExecuteTool.executeScript("routerRule", "transDateTimeHour", $event),"=","12"));
	then
		analyzeResultProcessTool.processAnalyzeResult($event, "2");
	end

看到上面最终生成的drl文件和Drools简介中给的样例结构一样了,只是稍微复杂了点嵌套了很多函数,下面我们逐个分析。

三、原理详解

为了能让故事进行下去,假装回忆下Velocity的基本使用,因为我们使用此模板引擎来生成drl文件的,脑子里对着玩意空空的话没法听故事了。

1. Velocity基本使用

1.1 引入pom

  <dependency>
      <groupId>org.apache.velocity</groupId>
      <artifactId>velocity</artifactId>
      <version>1.7</version>
  </dependency>

1.2 编写VM

在这里插入图片描述

1.3 编写测试类

/**
 * @author Kkk
 * @date 2023/3/4
 */
public class Test {
    public static void main(String[] args) throws Exception{
        //设置velocity资源加载器
        Properties prop = new Properties();
        prop.put("file.resource.loader.class", ClasspathResourceLoader.class.getName());
        Velocity.init(prop);

        //创建Velocity容器
        VelocityContext context = new VelocityContext();
        context.put("name", "kkk");

        //加载模板
        Template tpl = Velocity.getTemplate("vms/velocitydemo.vm", "UTF-8");

        //合并数据到模板
        StringWriter writer = new StringWriter();
        tpl.merge(context, writer);
        System.out.println(writer.toString());

        writer.close();
    }
}

打印结果:

hello,kkk!

变量引用:
对引擎上下文对象中的属性进行操作。语法方面分为常规语法(属性 ) 和正规语法 ( 属性)和正规语法(属性)和正规语法({属性})。
语法

$变量名, 若上下文中没有对应的变量,则输出字符串"$变量名"
$!变量名, 若上下文中没有对应的变量,则输出空字符串""
${变量名},若上下文中没有对应的变量,则输出字符串"${变量名}"
$!{变量名}, 若上下文中没有对应的变量,则输出空字符串""

Velocity Template Language (VTL) , 是Velocity 中提供的一种模版语言 , 旨在提供最简单和最干净的方法来将动态内容合并到网页中

VTL的语句分为4大类:注释 , 非解析内容, 引用和指令。

看懂以上demo那说明你已经了解了VTL语句变量引用了!

1.4 VTL的语法

1.4.1 方法引用

$变量名.方法(入参), 常规写法
${变量名.方法(入参), 正规写法
$!变量名.方法(入参), 常规写法
$!{变量名.方法((入参)}, 正规写法

创建Person类:

/**
 * @author Kkk
 * @date 2023/3/4
 */
public class Person {
    private String name;
    private int age;
    //...省略...
}

修改VM:

hello,$person.getName()!
how old ?
$person.getAge()!

测试类:

/**
 * @author Kkk
 * @date 2023/3/4
 */
public class Test {
    public static void main(String[] args) throws Exception{
        //设置velocity资源加载器
        Properties prop = new Properties();
        prop.put("file.resource.loader.class", ClasspathResourceLoader.class.getName());
        Velocity.init(prop);

        //创建Velocity容器
        Person person = new Person("kkk",18);
        VelocityContext context = new VelocityContext();
        context.put("person", person);

        //加载模板
        Template tpl = Velocity.getTemplate("vms/velocitydemo.vm", "UTF-8");

        //合并数据到模板
        StringWriter writer = new StringWriter();
        tpl.merge(context, writer);
        System.out.println(writer.toString());

        writer.close();
    }
}

1.4.2 指令–foreach

#foreach($item in $items)
    ..........
    [#break]
#end
  • $items : 需要遍历的对象或者集合
    • 如果items的类型为map集合, 那么遍历的是map的value
  • $item : 变量名称, 代表遍历的每一项
  • #break : 退出循环
  • 内置属性 :
    • $foreach.index : 获取遍历的索引 , 从0开始
    • $foreach.count : 获取遍历的次数 , 从1开始

本篇不是讲模板引擎的,所以稍微稍微提了下基本使用,了解了这几个引用和指令就可以接着听故事了。

2. drl文件生成分析

package com.kyn.router
import java.lang.*;
import java.util.*;
import com.kyn.router.service.runtime.tools.*;
import com.kyn.router.domain.entity.*;
global com.kyn.router.service.runtime.tools.ScritpExecuteTool scritpExecuteTool;
global com.kyn.router.service.runtime.tools.EventPropertyObtainTool eventPropertyObtainTool;
global com.kyn.router.service.runtime.tools.AnalyzeResultProcessTool analyzeResultProcessTool;
#foreach($ruleEntity in $ruleEntities)
    #if(!$ruleEntity.getRuleConditionEntities().isEmpty())
        $ruleTemplateGenerator.generateRuleTemplate($ruleEntity)
    #end
#end

从我们定义的模板中可以看到我们定义了多个工具类,在分析工具类之前我们首先看下我们的实体类ruleEntities的结果
RuleCompareTool
ScritpExecuteTool scritpExecuteTool
EventPropertyObtainTool eventPropertyObtainTool
AnalyzeResultProcessTool analyzeResultProcessTool

2.1 ruleEntities分析

在这里插入图片描述
如上截图已经很清晰了, ruleEntities包含了
规则表(router_rule)和条件表(rule_condition)数据,

#foreach($ruleEntity in $ruleEntities)
    #if(!$ruleEntity.getRuleConditionEntities().isEmpty())
        $ruleTemplateGenerator.generateRuleTemplate($ruleEntity)
    #end
#end

了解了ruleEntities结构也就很容易能看懂这个vm的前两行了,总共三行,已经可以看懂两行了,接着我们分析第三行。

2.2 完整规则模板分析

ruleTemplateGenerator类中重要方法:

    /**
     * 单条规则模板生成
     * @param ruleEntity
     * @return
     */
    @Override
    public String generateRuleTemplate(IRuleEntity ruleEntity) {
        Context context = new VelocityContext();
        context.put("ruleEntity", ruleEntity);
        logger.info("单个领域规则模型为{}", JSON.toJSONString(ruleEntity));
        StringWriter writer = new StringWriter();
        try {
            engine.mergeTemplate(ruleTemplate, "UTF-8", context, writer);
        } catch (Exception e) {
            logger.error("生成规则文件异常", e);
            throw new RouterException(SystemErrorCode.BIZ_CHECK_ERROR, new Object[]{"生成规则文件异常"});
        }
        logger.info("生成的单个规则文件内容为{}", writer.toString());
        return writer.toString();
    }

    /**
     * 利用 Velocity 生成规则文件 完整 drl文件
     * @param ruleEntities
     * @return
     */
    @Override
    public String generateDroolDRLTemplate(Collection<IRuleEntity> ruleEntities) {
        Context context = new VelocityContext();
        context.put("ruleEntities", ruleEntities);
        context.put("ruleTemplateGenerator", this);
        StringWriter writer = new StringWriter();
        try {
            engine.mergeTemplate(completeTemplate, "UTF-8", context, writer);
        } catch (Exception e) {
            logger.error("生成规则模板异常", e);
            throw new RouterException("生成规则模板异常", e, SystemErrorCode.BIZ_CHECK_ERROR);
        }
        return writer.toString();
    }

系统启动时,查询规则表、条件表构建ruleEntities后调用ruleTemplateGeneratorgenerateDroolDRLTemplate方法生成drl文件,generateDroolDRLTemplate对应完整规则文件模板,在完整规则文件模板中又会调用 generateRuleTemplate方法, generateRuleTemplate方法对应单条规则模板
所以在完整规则模板中的第三个代码就会跳转到单条规则模板中。

$ruleTemplateGenerator.generateRuleTemplate($ruleEntity)

2.3 单条规则模板分析

下面我们分析单条规则模板:

rule "$ruleEntity.getId()"
when
#foreach($factExpression in $ruleEntity.getFactExpressions())
	$!factExpression
#end
#foreach($conditon in $ruleEntity.getRuleConditionEntities())
	$!conditon.getDroolsExpression()
#end
then
	$ruleEntity.getThenExpression()
end

如上规则可以分为三部分:
第一部分ruleName部分
第二部分LHS部分
第三部分RHS部分

首先看第一部分

rule "$ruleEntity.getId()"

规则名称部分,此部分不重要,但是不能重复,我们使用规则在数据库中的主键Id作为规则名称。

首先看第二部分

#foreach($factExpression in $ruleEntity.getFactExpressions())
	$!factExpression
#end

$ruleEntity.getFactExpressions()可以看到ruleEntity提供了个方法,并且返回了个集合,此部分比较简单只是返回了个赋值字符串:$event : RuleCenterEvent();
RuleCenterEvent又是什么?这个对象是我们定义的FACT对象,FACT对象又是什么?不做解释了,再解释就没完了,接着我们看下RuleCenterEvent类结构:

/**
 * @author Kkk
 * @Description: 规则事件属性
 */
public class RuleCenterEvent {

    private Map<String, Object> extendProperties;

    public Map<String, Object> getExtendProperties() {
        return extendProperties;
    }

    public void setExtendProperties(Map<String, Object> extendProperties) {
        this.extendProperties = extendProperties;
    }

    public void addExtendProperty(String key, Object value) {
        if (extendProperties == null) {
            extendProperties = new HashMap<String, Object>();
        }
        extendProperties.put(key, value);
    }

    public Object getExtendPropertiesVlue(String key) {
        return extendProperties.get(key);
    }
}

结构比较简单,这里先不再展开,因为这个是在触发规则匹配时候传入的FACT对象。

#foreach($conditon in $ruleEntity.getRuleConditionEntities())
	$!conditon.getDroolsExpression()
#end

又是一个foreach,上一部分的foreach就目前业务使用来说完全可以固定写死成一个字符串,如可以直接写成如下结构:

rule "$ruleEntity.getId()"
when
$event : RuleCenterEvent();
#foreach($conditon in $ruleEntity.getRuleConditionEntities())
	$!conditon.getDroolsExpression()
#end
then
	$ruleEntity.getThenExpression()
end

接着分析第二部分,根据名字大概可以猜到干了什么,即遍历规则条件生成drlLHS部分的表达式,
我们看下这部分$!conditon.getDroolsExpression()代码:

    public String getDroolsExpression() {
        RuleConfigEntity ruleConfigEntity = RuleConfigCache.getCacheRuleConfig(this.getRuleScene(), this.getConfigKey());
        if (ruleConfigEntity.getIsScript().equals(BooleanEnum.YES.getCode())) {
            return getScriptExpression(this);
        } else {
            return getEventPropertyExpression(this);
        }
    }

要看懂这部分就要结合路由配置流程了,在配置路由规则条件部分时配置编码来源是来自条件配置管理提前配置的配置,所以我们获取到规则的条件后,根据条件的ruleScene(这列为配置页面暗含的项)和configKey(配置编码),就能反查到条件配置页面配置的这个条件是脚本还是普通条件。
在这里插入图片描述
在这里插入图片描述

脚本模式:

首先看如果是配置脚本:

    /**
     * 获取脚本模式的表达式
     */
    private String getScriptExpression(RuleConditionEntity ruleConditionEntity) {
        String scriptExpression = String.format(
                "eval(RuleCompareTool.compare(scritpExecuteTool.executeScript(\"%s\", \"%s\", $event),\"%s\",\"%s\"));"
                , ruleConditionEntity.getRuleScene()
                , ruleConditionEntity.getConfigKey()
                , ruleConditionEntity.getComparison()
                , ruleConditionEntity.getValue())
                ;
        return scriptExpression;
    }

如上代码会生成如下字符串:

eval(RuleCompareTool.compare(scritpExecuteTool.executeScript(“routerRule”, “transDateTimeHour”, $event),“=”,“12”));

普通模式:
即如果我们配置的非脚本,只需要简单的=、>、<、in、!in、reg等直接进行比较,不需要计算后结果再比较的普通模式,一般都是普通模式:

    /**
     * 规则是默认属性时的DROOLS表达式
     * @param ruleConditionEntity
     * @return
     */
    public String getEventPropertyExpression(RuleConditionEntity ruleConditionEntity) {
        String expression = String.format(
                "eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, \"%s\"),\"%s\",\"%s\"));"
                , ruleConditionEntity.getConfigKey()
                , ruleConditionEntity.getComparison()
                , ruleConditionEntity.getValue())
                ;
        return expression;
    }

如上代码会生成如下字符串:

eval(RuleCompareTool.compare(eventPropertyObtainTool.getEventProperty($event, “productCode”),“=”,“test01”));

接着我们看第三部分:
then部分,即为输出部分,

then
	$ruleEntity.getThenExpression()
end
    /**
     * 获取THEN表达式
     * @return
     */
    default String getThenExpression() {
        String expression = String.format(
                "analyzeResultProcessTool.processAnalyzeResult($event, \"%s\");"
                , this.getId())
                ;
        return expression;
    }

如上代码会生成如下字符串:

analyzeResultProcessTool.processAnalyzeResult($event, “2”);

经过如上两个模板后就能生成我们所需要的drl文件了,文件中的函数后期再详细分析。


总结

面对常规设计下的横向扩展问题,我们提出了纵向扩展的解决方案,并使用规则引擎Drools实现这种纵向扩展,做好了技术选型,我们面对的第一个问题就是如何将
条件表(rule_condition)中的数据转换到 LHS 部分,将规则(router_rule)转换到 RHS 部分,为了生成规则引擎所需要的drl文件,我们使用模板引擎生成。本篇简单的介绍了借助模板引擎生成规则引擎所需要的drl文件。


版权声明:本文为qq_38940885原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。