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

双鬼带单

共 13398字,需浏览 27分钟

 · 2024-03-28

上篇回顾

上一篇地址
掘金
简书

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

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

4c1eb2ac6c5d10fddb9adc2aa38eb20b.webp
  1. 多层级的多个条件的逻辑组合

18bfb7f5c8ca664f64f7f9406828258b.webp表达式的设计

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

{
  "ruleParam""芝麻分",
  "operator""大于",
  "args": ["650"]
}

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

单层级多个表达式组合

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


{
  "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": ["失信"] }
  ],
}

多层级多个表达式组合

18bfb7f5c8ca664f64f7f9406828258b.webp
同理可以写出


{
  "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 实现的表达式对应的模型代码

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 )

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 值

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 的使用相当简单,代码如下

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 中可以正常使用

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(nullnullnull);
  }

  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, nullnullnull, 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 版本上进行测试

下篇文章提供相关代码


浏览 5
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报