风控规则引擎(二):多个条件自由组合的实现,如何将 Java 字符串转换成 Java 对象

上篇回顾

上一篇地址
掘金
简书

在上一篇中介绍了一个单独的动态表达式是如何执行的,这里讲一下多个表达式不同组合情况下的实现。这里主要介绍下面 2 种情况的设计,其他可自行扩展

  1. 单层级的多个条件的逻辑组合
  1. 多层级的多个条件的逻辑组合

表达式的设计

在上一篇中使用下面的格式表示了单个表示式,这种格式无法表示多个表达式组合的情况。

json 复制代码
{
  "ruleParam": "芝麻分",
  "operator": "大于",
  "args": ["650"]
}

针对这种多个表达式多层级的情况,修改表达式的定义,增加逻辑组合的设计

单层级多个表达式组合

通过增加 relation, type, children 来处理表达式层级关系

json 复制代码
{
  "relation": "or",  // 标记逻辑关系,取值 or, and
  "type": "logic",   // 标记当前节点类型,取值 logic, expression
  "children": [      // logic 类型节点需要子节点
    {
      "type": "expression",
    	"ruleParam": "芝麻分",
    	"operator": "大于",
    	"args": ["750"] 
    }, 
    { "type": "expression", "ruleParam": "微信支付分", "operator": "大于", "args": ["600"] },
    { "type": "expression", "ruleParam": "征信", "operator": "不是", "args": ["失信"] }
  ],
}

多层级多个表达式组合

同理可以写出

json 复制代码
{
  "type": "logic",   // 标记当前节点类型,取值 logic, expression
  "relation": "or",  // 标记逻辑关系,取值 or, and
  "children": [      // logic 类型节点需要子节点
    { "type": "expression", "ruleParam": "芝麻分", "operator": "大于", "args": ["750"] }, 
    { "type": "expression", "ruleParam": "微信支付分", "operator": "大于", "args": ["600"] },
    { 
      "type": "logic",
      "relation": "and",
      "children": [
    		{ "type": "expression", "ruleParam": "征信", "operator": "不是", "args": ["失信"] },
    		{ "type": "expression", "ruleParam": "在贷笔数", "operator": "等于", "args": ["0"] }
      ]
    }
  ],
}

到了这里便完成了表达式的最终设计,下面是 Java 实现的表达式对应的模型代码

java 复制代码
public class RuleNodeConfig {

  private String type;
  private String relation;

  private String ruleParam;
  private String operator;
  private List<String> args;

  private List<RuleNodeConfig> children;
}

表达式的执行

使用表达式引擎来执行

可以通过解析上面的 JSON 字符串来生成对应的表达式片段 比如:

  1. ( 芝麻分 > 750) || ( 微信支付分 > 600) || ( ! 征信.equals("失信") )
  2. ( 芝麻分 > 750) || ( 微信支付分 > 600) || ( (! 征信.equals("失信") ) and ( 在贷笔数 == 0 ) )

然后由上一篇提到的表达式引擎去处理结果

动态编译成 Java 代码处理

在上一篇文章发完之后,也有一些评论在顾虑表达式引擎的执行性能问题,我也在上一篇中加入和性能对比,这里我在贴一下。简单说下结论,直接写 Java 代码比使用 AviatorScript 快 10 倍,比 Jexl3 快 20 倍,比 OGNL 快 30 倍。不过动态表达式虽然在性能上和 Java 代码相比有所损失,但是也到了每秒百万级,对于大部分系统耗时来自于对于变量的获取上而不是表达式的计算上。( MyBatis 中动态 SQL 的实现使用了 OGNL )

shell 复制代码
Benchmark                                         Mode  Cnt           Score           Error  Units
Java               thrpt    3    22225354.763 ±  12062844.831  ops/s
JavaClass          thrpt    3    21878714.150 ±   2544279.558  ops/s
JavaDynamicClass   thrpt    3    18911730.698 ±  30559558.758  ops/s
GroovyClass        thrpt    3    10036761.622 ±    184778.709  ops/s
Aviator            thrpt    3     2871064.474 ±   1292098.445  ops/s
Mvel               thrpt    3     2400852.254 ±     12868.642  ops/s
JSEL               thrpt    3     1570590.250 ±     24787.535  ops/s
Jexl               thrpt    3     1121486.972 ±     76890.380  ops/s
OGNL               thrpt    3      776457.762 ±    110618.929  ops/s
QLExpress          thrpt    3      385962.847 ±      3031.776  ops/s
SpEL               thrpt    3      245545.439 ±     11896.161  ops/s

不过还是有办法提高表达式的性能,这个方法就是将表达式直接编译成 Java 代码来执行

生成 Java 代码字符串

我们可以通过一定的规则将 ( 芝麻分 > 750) || ( 微信支付分 > 600) || ( ! 征信.equals("失信") ) 转换成对应的 Java 代码,下面提供一个转换后的示例,为了方式生成 Java 类名相同,类名规定为 JavaRule + 表达式的 MD5 值

java 复制代码
package org.example.dyscript.dynamicscript;

import java.util.Map;

public interface Rule {

  boolean execute(Map<String, Object> parameters);
}

// ----

package org.example.dyscript.dynamicscript;

import java.util.Map;

public class JavaRule{表示式字符串的 MD5 值} implements Rule {
    
  public boolean execute(Map<String, Object> parameters) {
      
    Integer 芝麻分 = (Integer) parameters.get("芝麻分");
    Integer 微信支付分 = (Integer) parameters.get("微信支付分");
    String 征信 = (String) parameters.get("征信");
      
    return ( 芝麻分 > 750) || ( 微信支付分 > 600) || ( ! 征信.equals("失信") );
  }
}

居我所知,可以使用 2 种方式将 Java 字符串转换为 Java 对象

  1. 使用 Groovy。因为 Groovy 的代码兼容 Java,所以可以直接使用 Groovy 提供的 GroovyClassLoader 来将 Java 字符串解析成 Java Class,然后通过反射的方法的得到对应的 Java 对象
  2. 使用 Java 提供的 javax.tools.JavaCompiler 来解析 Java 字符串得到 Java Class,然后通过反射的方法的得到对应的 Java 对象。

使用 Groovy 编译代码

GroovyClassLoader 的使用相当简单,代码如下

java 复制代码
package org.example.dyscript.compiler;

import groovy.lang.GroovyClassLoader;

import org.example.dyscript.dynamicscript.Rule;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.concurrent.ConcurrentHashMap;

public class GroovyCompiler {

  private static final GroovyCompiler compiler = new GroovyCompiler();

  public static GroovyCompiler getInstance() {
    return compiler;
  }

  private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
  private final ConcurrentHashMap<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>();

  public Rule loadNewInstance(String codeSource) throws Exception {
    if (codeSource != null && codeSource.trim().length() > 0) {
      Class<?> clazz = getCodeSourceClass(codeSource);
      if (clazz != null) {
        Object instance = clazz.newInstance();
        if (instance instanceof Rule) {
          return (Rule) instance;
        }
        else {
          throw new IllegalArgumentException("loadNewInstance error, "
              + "cannot convert from instance[" + instance.getClass() + "] to Rule");
        }
      }
    }
    throw new IllegalArgumentException("loadNewInstance error, instance is null");
  }

  private Class<?> getCodeSourceClass(String codeSource) {
    try {
      byte[] md5 = MessageDigest.getInstance("MD5").digest(codeSource.getBytes());
      String md5Str = new BigInteger(1, md5).toString(16);
      Class<?> clazz = CLASS_CACHE.get(md5Str);
      if (clazz == null) {
        clazz = groovyClassLoader.parseClass(codeSource);
        CLASS_CACHE.putIfAbsent(md5Str, clazz);
      }
      return clazz;
    }
    catch (Exception e) {
      return groovyClassLoader.parseClass(codeSource);
    }
  }
}

使用 javax.tools.JavaCompiler 来编译代码

javax.tools.JavaCompiler 的使用相对麻烦些,以下代码不保证在不同的 jdk 中可以正常使用

java 复制代码
package org.example.dyscript.compiler;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

public class JavaStringCompiler {

  JavaCompiler compiler;
  StandardJavaFileManager stdManager;

  public JavaStringCompiler() {
    this.compiler = ToolProvider.getSystemJavaCompiler();
    this.stdManager = compiler.getStandardFileManager(null, null, null);
  }

  public Map<String, byte[]> compile(String fileName, String source) throws IOException {
    try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
      JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
      CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
      Boolean result = task.call();
      if (result == null || !result) {
        throw new RuntimeException("Compilation failed.");
      }
      return manager.getClassBytes();
    }
  }

  public Class<?> loadClass(String name, Map<String, byte[]> classBytes) throws ClassNotFoundException, IOException {
    try (MemoryClassLoader classLoader = new MemoryClassLoader(classBytes)) {
      return classLoader.loadClass(name);
    }
  }
}

总结

这是写的规则引擎的第二篇,主要讲一下

  1. 多个表示式自由组合是如何处理的
  2. 为了解决损失的那一点性能提供两种将 Java 代码直接转成对 Java 对象的方法,使用这种方式性能于直接使用 Java 硬编码相同
  3. 使用 Groovy 来编译代码更加安全可靠,javax.tools.JavaCompiler 则需要在不同的 JDK 版本上进行测试

下篇文章提供相关代码

相关推荐
小蒜学长8 分钟前
springboot党建云课堂学习与管理系统(代码+数据库+LW)
java·数据库·spring boot·后端·学习
烂蜻蜓2 小时前
Flask入门教程(十二):表单处理——从基础到Flask-WTF完整实践
后端·python·flask
2601_962055979 小时前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat
Lost of 程序猿10 小时前
ASP.NET Core API 幂等性设计深度实战:从一次重复申领事故说起
后端·asp.net
QQ_216962909611 小时前
【源码编号:project79475】SpringBoot校内二手交易平台:商品发布、分类检索、留言交流、订单管理全流程实战
java·spring boot·后端
郑州光合科技余经理13 小时前
本地生活服务系统:模块边界与结算字段怎么拆
java·开发语言·前端·后端·系统架构·uni-app·php
IT_陈寒13 小时前
Vue的嵌套组件竟然吃掉了我的事件?
前端·人工智能·后端
大辉狼_音频架构14 小时前
进阶:从源码编译 SOF 固件与 topology
后端
大辉狼_音频架构14 小时前
上板:让 SOF 在 FRDM-i.MX8MP 上跑起来
后端
大辉狼_音频架构14 小时前
FRDM-IMX8MP UUU 烧录 eMMC 指南
后端