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

下篇文章提供相关代码

相关推荐
凡人的AI工具箱5 分钟前
15分钟学 Python 第38天 :Python 爬虫入门(四)
开发语言·人工智能·后端·爬虫·python
丶213638 分钟前
【SQL】深入理解SQL:从基础概念到常用命令
数据库·后端·sql
木子020440 分钟前
Nacos的应用
后端
哎呦没40 分钟前
Spring Boot框架在医院管理中的应用
java·spring boot·后端
陈序缘1 小时前
Go语言实现长连接并发框架 - 消息
linux·服务器·开发语言·后端·golang
络71 小时前
Spring14——案例:利用AOP环绕通知计算业务层接口执行效率
java·后端·spring·mybatis·aop
2401_857600952 小时前
明星周边销售网站开发:SpringBoot技术全解析
spring boot·后端·php
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_2518364573 小时前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
qq_2518364573 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端