zoukankan      html  css  js  c++  java
  • 复杂的动态布尔表达式性能评估(2)--Groovy实现

    前言:
      规则引擎中, 往往涉及到多个条件构成了复杂布尔表达式的计算. 对于这类布尔表达式, 一是动态可变的(取决于运营人员的设定), 二是其表达式往往很复杂. 如何快速的计算其表达式的值, 该系列文章将以两种方式, Antlr4动态生成AST(抽象语法树), 以及Groovy动态编译的方式来对比评估, 看看哪种方式性能更优, 以及各自的优缺点. 本篇文章将侧重于Groovy的实现思路.

    模型简化:
      每个规则可以理解为多个条件构建的复杂布尔表达式, 而条件本身涉及不同的变量和阈值(常量), 以及中间的操作符(>=, >, <, <=, !=, =). 
      比如某个具体的规则:

    rule = expr1 && (expr2 || expr3) || expr4

      而其具体条件expr1/expr2/expr3/expr4如下:

    expr1 => var1 >= 20
    expr2 => var2 != 10
    expr3 => var3 < 3.0
    expr4 => var4 = true

      为了简化评估, 我们简单设定每个条件就是一个布尔变量(bool). 这样每个规则rule就可以理解为多个布尔变量, 通过&&和||组合的表达式了, 简单描述为:

    rule = 1 && (2 || 3) || 4

      数字N(1,2,...)为具体的布尔变量, 类似这样的简化模型, 方便性能评估. 

    Groovy实现:
      先配置maven的依赖.

            <dependency>
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy-all</artifactId>
                <version>2.4.13</version>
            </dependency>

      然后编写Groovy脚本的执行工具类:

    package com.dsl.perfs;
    
    import groovy.lang.Binding;
    import groovy.lang.GroovyClassLoader;
    import org.codehaus.groovy.control.CompilationFailedException;
    import org.codehaus.groovy.runtime.InvokerHelper;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class GroovyShellUtils {
    
        private static ConcurrentHashMap<String, Class> scriptClassMap = new ConcurrentHashMap();
    
        public static <T> T execExpr(String expr, Map<String, Object> params, Class<T> returnType) {
            if(expr == null || expr.length() == 0) {
                return null;
            } else {
                Object result = null;
    
                try {
                    Class e = parseClass(expr);
                    result = InvokerHelper.createScript(e, new Binding(params)).run();
                    return (T)result;
                } catch (Exception var5) {
                    return null;
                }
            }
        }
    
        public static Class parseClass(String scriptText) throws CompilationFailedException {
            String key = keyGen(scriptText);
            Class value = (Class)scriptClassMap.get(key);
            if(value != null) {
                return value;
            } else {
                synchronized(scriptText.intern()) {
                    if(scriptClassMap.get(key) == null) {
                        GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader());
                        Class scriptClass = groovyClassLoader.parseClass(scriptText);
                        scriptClassMap.put(key, scriptClass);
                        return scriptClass;
                    }
                }
    
                return (Class)scriptClassMap.get(key);
            }
        }
    
        private static String keyGen(String script) {
            return String.valueOf(script.intern().hashCode());
        }
    }

      具体执行时, 采用一个trick的方式, 将数值变量化(统一添加变量名前缀).
      比如把表达式:

    1 && 2 || 3 || 4 && (5 || 6)

      转化为

    t1 && t2 || t3 || t4 && (t5 || t6)


    测试评估:
      具体的测试代码为:

    package com.dsl.comp;
    
    import com.dsl.perfs.GroovyShellUtils;
    
    import java.util.Map;
    import java.util.Random;
    import java.util.TreeMap;
    
    
    public class AntlrPerf {
    
        public static void main(String[] args) {
    
            String boolExpr = "1 && 2 || 3 || 4 && (5 || 6)";
    
            int iterNums = 1000000;
            long randomSeed = 10001L;
    
            String nboolExpr = boolExpr;
            nboolExpr = nboolExpr.replace("1", "t1");
            nboolExpr = nboolExpr.replace("2", "t2");
            nboolExpr = nboolExpr.replace("3", "t3");
            nboolExpr = nboolExpr.replace("4", "t4");
            nboolExpr = nboolExpr.replace("5", "t5");
            nboolExpr = nboolExpr.replace("6", "t6");
    
            long beg = System.currentTimeMillis();
            random.setSeed(randomSeed);
            for ( int i = 0; i <= iterNums; i++) {
                Map<String, Object> params = new TreeMap<>();
                params.put("t1", random.nextBoolean());
                params.put("t2", random.nextBoolean());
                params.put("t3", random.nextBoolean());
                params.put("t4", random.nextBoolean());
                params.put("t5", random.nextBoolean());
                params.put("t6", random.nextBoolean());
    
                GroovyShellUtils.execExpr(nboolExpr, params, Boolean.class);
            }
            long end = System.currentTimeMillis();
            System.out.println(String.format("total consume: %dms", end - beg));
    
        }
    
    }

      测试结果如下:

    total consume: 1039ms

      和上篇Antlr4方案的测试结果755ms, 1039ms相对慢一些, 但总结而言差不多, 事实上, 无论采用哪种方案, 对于具体的线上服务而言, 其永远不是主要的性能瓶颈.

    优缺点分析:
      从性能结果上看, Antlr4动态解析的方案有一定的优势. 另一方面, 采用Groovy的方案, 对应的表示式会生成一个对应的Class类, 表达式越多, 生成的Class越多, 对方法区的消耗也不小. 由于JIT的存在, 会将热点的代码编译生成native code, 用于代码的加速执行. 但是该native code区域的空间相对较小, 满了会影响性能.
      但是从灵活性和场景适用范围而言, Groovy方案几乎完胜, Antlr4的编码成本太高, 尤其是面对复杂的逻辑时.

    总结:
      本文也是借助复杂布尔表达式的评估, 来简单比较下Antlr方案和Groovy方案的差异. 条条大路通罗马, 其实那个方案都合理.

  • 相关阅读:
    关于React的脚手架
    yarn和npm
    谈谈NPM和Webpack的关系
    php开发环境和框架phalcon的搭建
    Centos6.5--svn搭建
    System.Diagnostics.Process.Start(ProcessStartInfo)
    PHP错误:call to undefined function imagecreatetruecolor
    PostgreSQL删除表中重复数据行
    URL存在http host头攻击漏洞-修复方案
    for循环的执行顺序
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/8480179.html
Copyright © 2011-2022 走看看