风控规则引擎(二):多个条件自由组合的实现,如何将 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 版本上进行测试

下篇文章提供相关代码

相关推荐
monkey_meng11 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss19 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万1 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
转转技术团队1 小时前
空间换时间-将查询数据性能提升100倍的计数系统实践
java·后端·架构
酸奶代码2 小时前
Spring AOP技术
java·后端·spring
代码小鑫2 小时前
A034-基于Spring Boot的供应商管理系统的设计与实现
java·开发语言·spring boot·后端·spring·毕业设计
paopaokaka_luck2 小时前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法