Coder Social home page Coder Social logo

alibaba / qlexpress Goto Github PK

View Code? Open in Web Editor NEW
4.5K 213.0 1.1K 2.31 MB

QLExpress is a powerful, lightweight, dynamic language for the Java platform aimed at improving developers’ productivity in different business scenes.

License: Apache License 2.0

Java 99.42% CodeQL 0.58%

qlexpress's Introduction

QLExpress基本语法

Join the chat at https://gitter.im/QLExpress/Lobby

一、背景介绍

由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。 在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。

QLExpress脚本引擎被广泛应用在阿里的电商业务场景,具有以下的一些特性:

  • 1、线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型。
  • 2、高效执行,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当。
  • 3、弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。
  • 4、安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。
  • 5、代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。

二、依赖和调用说明

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>QLExpress</artifactId>
  <version>3.3.3</version>
</dependency>

版本兼容性: 跳转查看

ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
String express = "a + b * c";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);

如果应用有让终端用户输入与执行 QLExpress 的功能,务必关注 多级别安全控制将 QLExpress 的安全级别配置在白名单或以上。

三、语法介绍

1、操作符和java对象操作

普通java语法

//支持 +,-,*,/,<,>,<=,>=,==,!=,<>【等同于!=】,%,mod【取模等同于%】,++,--,
//in【类似sql】,like【sql语法】,&&,||,!,等操作符
//支持for,break、continue、if then else 等标准的程序控制逻辑
n = 10;
sum = 0;
for(i = 0; i < n; i++) {
   sum = sum + i;
}
return sum;

//逻辑三元操作
a = 1;
b = 2;
maxnum = a > b ? a : b;

和java语法相比,要避免的一些ql写法错误

  • 不支持try{}catch{}
  • 注释目前只支持 /** **/,不支持单行注释 //
  • 不支持java8的lambda表达式
  • 不支持for循环集合操作for (Item item : list)
  • 弱类型语言,请不要定义类型声明,更不要用Template(Map<String, List>之类的)
  • array的声明不一样
  • min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名
//java语法:使用泛型来提醒开发者检查类型
keys = new ArrayList<String>();
deviceName2Value = new HashMap<String, String>(7);
String[] deviceNames = {"ng", "si", "umid", "ut", "mac", "imsi", "imei"};
int[] mins = {5, 30};

//ql写法:
keys = new ArrayList();
deviceName2Value = new HashMap();
deviceNames = ["ng", "si", "umid", "ut", "mac", "imsi", "imei"];
mins = [5, 30];

//java语法:对象类型声明
FocFulfillDecisionReqDTO reqDTO = param.getReqDTO();
//ql写法:
reqDTO = param.getReqDTO();

//java语法:数组遍历
for(Item item : list) {
}
//ql写法:
for(i = 0; i < list.size(); i++){
    item = list.get(i);
}

//java语法:map遍历
for(String key : map.keySet()) {
    System.out.println(map.get(key));
}
//ql写法:
keySet = map.keySet();
objArr = keySet.toArray();
for (i = 0; i < objArr.length; i++) {
    key = objArr[i];
    System.out.println(map.get(key));
}

java的对象操作

import com.ql.util.express.test.OrderQuery;
//系统自动会import java.lang.*,import java.util.*;

query = new OrderQuery();           // 创建class实例,自动补全类路径
query.setCreateDate(new Date());    // 设置属性
query.buyer = "张三";                // 调用属性,默认会转化为setBuyer("张三")
result = bizOrderDAO.query(query);  // 调用bean对象的方法
System.out.println(result.getId()); // 调用静态方法

2、脚本中定义function

function add(int a, int b){
    return a + b;
};

function sub(int a, int b){
    return a - b;
};

a = 10;
return add(a, 4) + sub(a, 9);

3、扩展操作符:Operator

替换 if then else 等关键字

runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("则", "then", null);
runner.addOperatorWithAlias("否则", "else", null);

express = "如果 (语文 + 数学 + 英语 > 270) 则 {return 1;} 否则 {return 0;}";
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.execute(express, context, null, false, false, null);

如何自定义Operator

import java.util.ArrayList;
import java.util.List;

/**
 * 定义一个继承自com.ql.util.express.Operator的操作符
 */
public class JoinOperator extends Operator {
    public Object executeInner(Object[] list) throws Exception {
        Object opdata1 = list[0];
        Object opdata2 = list[1];
        if (opdata1 instanceof List) {
            ((List)opdata1).add(opdata2);
            return opdata1;
        } else {
            List result = new ArrayList();
            for (Object opdata : list) {
                result.add(opdata);
            }
            return result;
        }
    }
}

如何使用Operator

//(1)addOperator
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addOperator("join", new JoinOperator());
Object r = runner.execute("1 join 2 join 3", context, null, false, false);
System.out.println(r); // 返回结果 [1, 2, 3]

//(2)replaceOperator
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.replaceOperator("+", new JoinOperator());
Object r = runner.execute("1 + 2 + 3", context, null, false, false);
System.out.println(r); // 返回结果 [1, 2, 3]

//(3)addFunction
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addFunction("join", new JoinOperator());
Object r = runner.execute("join(1, 2, 3)", context, null, false, false);
System.out.println(r); // 返回结果 [1, 2, 3]

4、绑定java类或者对象的method

addFunctionOfClassMethod + addFunctionOfServiceMethod

public class BeanExample {
    public static String upper(String abc) {
        return abc.toUpperCase();
    }
    public boolean anyContains(String str, String searchStr) {
        char[] s = str.toCharArray();
        for (char c : s) {
            if (searchStr.contains(c+"")) {
                return true;
            }
        }
        return false;
    }
}

runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs", new String[] {"double"}, null);
runner.addFunctionOfClassMethod("转换为大写", BeanExample.class.getName(), "upper", new String[] {"String"}, null);

runner.addFunctionOfServiceMethod("打印", System.out, "println", new String[] { "String" }, null);
runner.addFunctionOfServiceMethod("contains", new BeanExample(), "anyContains", new Class[] {String.class, String.class}, null);

String express = "取绝对值(-100); 转换为大写(\"hello world\"); 打印(\"你好吗?\"); contains("helloworld",\"aeiou\")";
runner.execute(express, context, null, false, false);

5、macro 宏定义

runner.addMacro("计算平均成绩", "(语文+数学+英语)/3.0");
runner.addMacro("是否优秀", "计算平均成绩>90");
IExpressContext<String, Object> context = new DefaultContext<String, Object>();
context.put("语文", 88);
context.put("数学", 99);
context.put("英语", 95);
Object result = runner.execute("是否优秀", context, null, false, false);
System.out.println(r);
//返回结果true

6、编译脚本,查询外部需要定义的变量和函数。

注意以下脚本int和没有int的区别

String express = "int 平均分 = (语文 + 数学 + 英语 + 综合考试.科目2) / 4.0; return 平均分";
ExpressRunner runner = new ExpressRunner(true, true);
String[] names = runner.getOutVarNames(express);
for(String s:names){
    System.out.println("var : " + s);
}

//输出结果:
var : 数学
var : 综合考试
var : 英语
var : 语文

7、关于不定参数的使用

@Test
public void testMethodReplace() throws Exception {
    ExpressRunner runner = new ExpressRunner();
    IExpressContext<String, Object> expressContext = new DefaultContext<String, Object>();
    runner.addFunctionOfServiceMethod("getTemplate", this, "getTemplate", new Class[]{Object[].class}, null);

    //(1)默认的不定参数可以使用数组来代替
    Object r = runner.execute("getTemplate([11,'22', 33L, true])", expressContext, null, false, false);
    System.out.println(r);
    //(2)像java一样,支持函数动态参数调用,需要打开以下全局开关,否则以下调用会失败
    DynamicParamsUtil.supportDynamicParams = true;
    r = runner.execute("getTemplate(11, '22', 33L, true)", expressContext, null, false, false);
    System.out.println(r);
}

//等价于getTemplate(Object[] params)
public Object getTemplate(Object... params) throws Exception{
    String result = "";
    for(Object obj:params){
        result = result + obj + ",";
    }
    return result;
}

8、关于集合的快捷写法

@Test
public void testSet() throws Exception {
    ExpressRunner runner = new ExpressRunner(false, false);
    DefaultContext<String, Object> context = new DefaultContext<String, Object>();
    String express = "abc = NewMap(1:1, 2:2); return abc.get(1) + abc.get(2);";
    Object r = runner.execute(express, context, null, false, false);
    System.out.println(r);
    express = "abc = NewList(1, 2, 3); return abc.get(1) + abc.get(2)";
    r = runner.execute(express, context, null, false, false);
    System.out.println(r);
    express = "abc = [1, 2, 3]; return abc[1] + abc[2];";
    r = runner.execute(express, context, null, false, false);
    System.out.println(r);
}

9、集合的遍历

其实类似java的语法,只是ql不支持for(obj:list){}的语法,只能通过下标访问。

//遍历map
map = new HashMap();
map.put("a", "a_value");
map.put("b", "b_value");
keySet = map.keySet();
objArr = keySet.toArray();
for (i = 0; i < objArr.length; i++) {
    key = objArr[i];
    System.out.println(map.get(key));
}

四、运行参数和API列表介绍

QLExpressRunner如下图所示,从语法树分析、上下文、执行过程三个方面提供二次定制的功能扩展。

QlExpress-detail.jpg

1、属性开关

isPrecise

/**
 * 是否需要高精度计算
 */
private boolean isPrecise = false;

高精度计算在会计财务中非常重要,java的float、double、int、long存在很多隐式转换,做四则运算和比较的时候其实存在非常多的安全隐患。 所以类似汇金的系统中,会有很多BigDecimal转换代码。而使用QLExpress,你只要关注数学公式本身 订单总价 = 单价 * 数量 + 首重价格 + ( 总重量 - 首重) * 续重单价 ,然后设置这个属性即可,所有的中间运算过程都会保证不丢失精度。

isShortCircuit

/**
 * 是否使用逻辑短路特性
 */
private boolean isShortCircuit = true;

在很多业务决策系统中,往往需要对布尔条件表达式进行分析输出,普通的java运算一般会通过逻辑短路来减少性能的消耗。例如规则公式: star > 10000 and shopType in ('tmall', 'juhuasuan') and price between (100, 900) 假设第一个条件 star>10000 不满足就停止运算。但业务系统却还是希望把后面的逻辑都能够运算一遍,并且输出中间过程,保证更快更好的做出决策。

参照单元测试:ShortCircuitLogicTest.java

isTrace

/**
 * 是否输出所有的跟踪信息,同时还需要log级别是DEBUG级别
 */
private boolean isTrace = false;

这个主要是是否输出脚本的编译解析过程,一般对于业务系统来说关闭之后会提高性能。

2、调用入参

/**
 * 执行一段文本
 * @param expressString 程序文本
 * @param context 执行上下文,可以扩展为包含ApplicationContext
 * @param errorList 输出的错误信息List
 * @param isCache 是否使用Cache中的指令集,建议为true
 * @param isTrace 是否输出详细的执行指令信息,建议为false
 * @param aLog 输出的log
 * @return
 * @throws Exception
 */
Object execute(String expressString, IExpressContext<String, Object> context, List<String> errorList, boolean isCache, boolean isTrace);

3、功能扩展API列表

QLExpress主要通过子类实现Operator.java提供的以下方法来最简单的操作符定义,然后可以被通过addFunction或者addOperator的方式注入到ExpressRunner中。

public abstract Object executeInner(Object[] list) throws Exception;

比如我们几行代码就可以实现一个功能超级强大、非常好用的join操作符:

_list = 1 join 2 join 3;_         -> [1,2,3]
_list = join(list, 4, 5, 6);_     -> [1,2,3,4,5,6]
import java.util.ArrayList;
import java.util.List;

public class JoinOperator extends Operator {
    public Object executeInner(Object[] list) throws Exception {
        List result = new ArrayList();
        Object opdata1 = list[0];
        if (opdata1 instanceof List) {
            result.addAll((List)opdata1);
        } else {
            result.add(opdata1);
        }
        for (int i = 1; i < list.length; i++) {
            result.add(list[i]);
        }
        return result;
    }
}

如果你使用Operator的基类OperatorBase.java将获得更强大的能力,基本能够满足所有的要求。

(1)function相关API

//通过name获取function的定义
OperatorBase getFunciton(String name);

//通过自定义的Operator来实现类似:fun(a, b, c)
void addFunction(String name, OperatorBase op);

//fun(a, b, c) 绑定 object.function(a, b, c)对象方法
void addFunctionOfServiceMethod(String name, Object aServiceObject, String aFunctionName, Class<?>[] aParameterClassTypes, String errorInfo);

//fun(a, b, c) 绑定 Class.function(a, b, c)类方法
void addFunctionOfClassMethod(String name, String aClassName, String aFunctionName, Class<?>[] aParameterClassTypes, String errorInfo);

//给Class增加或者替换method,同时支持 a.fun(b), fun(a, b) 两种方法调用
//比如扩展String.class的isBlank方法:"abc".isBlank()和isBlank("abc")都可以调用
void addFunctionAndClassMethod(String name, Class<?> bindingClass, OperatorBase op);

(2)Operator相关API

提到脚本语言的操作符,优先级、运算的目数、覆盖原始的操作符(+,-,*,/等等)都是需要考虑的问题,QLExpress统统帮你搞定了。

//添加操作符号,可以设置优先级
void addOperator(String name, Operator op);
void addOperator(String name, String aRefOpername, Operator op);

//替换操作符处理
OperatorBase replaceOperator(String name, OperatorBase op);

//添加操作符和关键字的别名,比如 if..then..else -> 如果。。那么。。否则。。
void addOperatorWithAlias(String keyWordName, String realKeyWordName, String errorInfo);

(3)宏定义相关API

QLExpress的宏定义比较简单,就是简单的用一个变量替换一段文本,和传统的函数替换有所区别。

//比如addMacro("天猫卖家", "userDO.userTag &1024 == 1024")
void addMacro(String macroName, String express);

(4)java class的相关api

QLExpress可以通过给java类增加或者改写一些method和field,比如 链式调用:"list.join("1").join("2")",比如中文属性:"list.长度"。

//添加类的属性字段
void addClassField(String field, Class<?>bindingClass, Class<?>returnType, Operator op);

//添加类的方法
void addClassMethod(String name, Class<?>bindingClass, OperatorBase op);

注意,这些类的字段和方法是执行器通过解析语法执行的,而不是通过字节码增强等技术,所以只在脚本运行期间生效,不会对jvm整体的运行产生任何影响,所以是绝对安全的。

(4)语法树解析变量、函数的API

这些接口主要是对一个脚本内容的静态分析,可以作为上下文创建的依据,也可以用于系统的业务处理。 比如:计算 "a + fun1(a) + fun2(a + b) + c.getName()" 包含的变量:a,b,c 包含的函数:fun1,fun2

//获取一个表达式需要的外部变量名称列表
String[] getOutVarNames(String express);
String[] getOutFunctionNames(String express);

(5)语法解析校验api

脚本语法是否正确,可以通过ExpressRunner编译指令集的接口来完成。

String expressString = "for(i = 0; i < 10; i++) {sum = i + 1;} return sum;";
InstructionSet instructionSet = expressRunner.parseInstructionSet(expressString);
//如果调用过程不出现异常,指令集instructionSet就是可以被加载运行(execute)了!

(6)指令集缓存相关的api

因为QLExpress对文本到指令集做了一个本地HashMap缓存,通常情况下一个设计合理的应用脚本数量应该是有限的,缓存是安全稳定的,但是也提供了一些接口进行管理。

//优先从本地指令集缓存获取指令集,没有的话生成并且缓存在本地
InstructionSet getInstructionSetFromLocalCache(String expressString);
//清除缓存
void clearExpressCache();

(7)安全风险控制

7.1 防止死循环

try {
    express = "sum = 0; for(i = 0; i < 1000000000; i++) {sum = sum + i;} return sum;";
    //可通过timeoutMillis参数设置脚本的运行超时时间:1000ms
    Object r = runner.execute(express, context, null, true, false, 1000);
    System.out.println(r);
    throw new Exception("没有捕获到超时异常");
} catch (QLTimeOutException e) {
    System.out.println(e);
}

7.1 防止调用不安全的系统api

更加详细多级安全控制见 多级别安全控制

ExpressRunner runner = new ExpressRunner();
QLExpressRunStrategy.setForbiddenInvokeSecurityRiskMethods(true);

DefaultContext<String, Object> context = new DefaultContext<String, Object>();
try {
    express = "System.exit(1);";
    Object r = runner.execute(express, context, null, true, false);
    System.out.println(r);
    throw new Exception("没有捕获到不安全的方法");
} catch (QLException e) {
    System.out.println(e);
}

(8)增强上下文参数Context相关的api

8.1 与spring框架的无缝集成

上下文参数 IExpressContext context 非常有用,它允许put任何变量,然后在脚本中识别出来。

在实际中我们很希望能够无缝的集成到spring框架中,可以仿照下面的例子使用一个子类。

public class QLExpressContext extends HashMap<String, Object> implements IExpressContext<String, Object> {
    private final ApplicationContext context;

    // 构造函数,传入context 和 ApplicationContext
    public QLExpressContext(Map<String, Object> map, ApplicationContext aContext) {
        super(map);
        this.context = aContext;
    }

    /**
     * 抽象方法:根据名称从属性列表中提取属性值
     */
    public Object get(Object name) {
        Object result;
        result = super.get(name);
        try {
            if (result == null && this.context != null && this.context.containsBean((String)name)) {
                // 如果在Spring容器中包含bean,则返回String的Bean
                result = this.context.getBean((String)name);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public Object put(String name, Object object) {
        return super.put(name, object);
    }
}

完整的demo参照 SpringDemoTest.java

8.2 自定义函数操作符获取原始的context控制上下文

自定义的Operator需要直接继承OperatorBase,获取到parent即可,可以用于在运行一组脚本的时候,直接编辑上下文信息,业务逻辑处理上也非常有用。

public class ContextMessagePutTest {
    class OperatorContextPut extends OperatorBase {
        public OperatorContextPut(String aName) {
            this.name = aName;
        }

        @Override
        public OperateData executeInner(InstructionSetContext parent, ArraySwap list) throws Exception {
            String key = list.get(0).toString();
            Object value = list.get(1);
            parent.put(key, value);
            return null;
        }
    }

    @Test
    public void test() throws Exception {
        ExpressRunner runner = new ExpressRunner();
        OperatorBase op = new OperatorContextPut("contextPut");
        runner.addFunction("contextPut", op);
        String express = "contextPut('success', 'false'); contextPut('error', '错误信息'); contextPut('warning', '提醒信息')";
        IExpressContext<String, Object> context = new DefaultContext<String, Object>();
        context.put("success", "true");
        Object result = runner.execute(express, context, null, false, true);
        System.out.println(result);
        System.out.println(context);
    }
}

4. 多级别安全控制

QLExpress 与本地 JVM 交互的方式有:

  • 应用中的自定义函数/操作符/宏: 该部分不在 QLExpress 运行时的管控范围,属于应用开放给脚本的业务功能,不受安全控制,应用需要自行确保这部分是安全的
  • 在 QLExpress 运行时中发生的交互: 安全控制可以对这一部分进行管理, QLExpress 会开放相关的配置给应用
    • 通过 . 操作符获取 Java 对象的属性或者调用 Java 对象中的方法
    • 通过 import 可以导入 JVM 中存在的任何类并且使用, 默认情况下会导入 java.lang, java.util 以及 java.util.stream

在不同的场景下,应用可以配置不同的安全级别,安全级别由低到高:

  1. 黑名单控制:QLExpress 默认会阻断一些高危的系统 API, 用户也可以自行添加, 但是开放对 JVM 中其他所有类与方法的访问, 最灵活, 但是很容易被反射工具类绕过,只适用于脚本安全性有其他严格控制的场景,禁止直接运行终端用户输入

  2. 白名单控制:QLExpress 支持编译时白名单和运行时白名单机制, 编译时白名单设置到类级别, 能够在语法检查阶段就暴露出不安全类的使用, 但是无法阻断运行时动态生成的类(比如通过反射), 运行时白名单能够确保运行时只可以直接调用有限的 Java 方法, 必须设置了运行时白名单, 才算是达到了这个级别

  3. 沙箱模式:QLExpress 作为一个语言沙箱, 只允许通过自定义函数/操作符/宏与应用交互, 不允许与 JVM 中的类产生交互

(1) 黑名单控制

QLExpess 目前默认添加的黑名单有:

  • java.lang.System.exit
  • java.lang.Runtime.exec
  • java.lang.ProcessBuilder.start
  • java.lang.reflect.Method.invoke
  • java.lang.reflect.Class.forName
  • java.lang.reflect.ClassLoader.loadClass
  • java.lang.reflect.ClassLoader.findClass

同时支持通过 QLExpressRunStrategy.addSecurityRiskMethod 额外添加

com.ql.util.express.example.MultiLevelSecurityTest#blockWhiteListControlTest

// 必须将该选项设置为 true
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
// 这里不区分静态方法与成员方法, 写法一致
// 不支持重载, riskMethod 的所有重载方法都会被禁止
QLExpressRunStrategy.addSecurityRiskMethod(RiskBean.class, "riskMethod");
ExpressRunner expressRunner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
try {
    expressRunner.execute("import com.ql.util.express.example.RiskBean;" +
                          "RiskBean.riskMethod()", context, null, true, false);
    fail("没有捕获到不安全的方法");
} catch (Exception e) {
    assertTrue(e.getCause() instanceof QLSecurityRiskException);
}

(2)白名单控制

编译期白名单:

编译期白名单是类维度的,脚本中只允许显式引用符合白名单条件的类,支持两种设置方式,精确设置某个类,以及设置某个类的全部子类。

com.ql.util.express.example.MultiLevelSecurityTest#compileWhiteListTest

// 设置编译期白名单
QLExpressRunStrategy.setCompileWhiteCheckerList(Arrays.asList(
    // 精确设置
    CheckerFactory.must(Date.class),
    // 子类设置
    CheckerFactory.assignable(List.class)
));
ExpressRunner expressRunner = new ExpressRunner();
// Date 在编译期白名单中, 可以显示引用
expressRunner.execute("new Date()", new DefaultContext<>(), null,
                      false, true);
// LinkedList 是 List 的子类, 符合白名单要求
expressRunner.execute("LinkedList ll = new LinkedList; ll.add(1); ll.add(2); ll",
                      new DefaultContext<>(), null, false, true);
try {
    // String 不在白名单中, 不可以显示引用
    // 但是隐式引用, a = 'mmm', 或者定义字符串常量 'mmm' 都是可以的
    expressRunner.execute("String a = 'mmm'", new DefaultContext<>(), null,
                          false, true);
} catch (Exception e) {
    assertTrue(e.getCause() instanceof QLSecurityRiskException);
}

// Math 不在白名单中
// 对于不满足编译期类型白名单的脚本无需运行, 即可通过 checkSyntax 检测出
assertFalse(expressRunner.checkSyntax("Math.abs(-1)"));

编译期白名单只能检测出脚本编译时能够确认的类型,任何运行时出现的类型都是无法检测的,诸如各种反射Class.forName, ClassLoader.loadClass,或者没有声明类型的变量等等,因为编译期白名单只能增加黑客的作案成本,是容易被绕过。因此建议编译期白名单只用来帮助脚本校验,如果需要接收终端用户输入,运行期白名单是务必要配置的。

运行期白名单:

如果有白名单设置,所有的黑名单设置就都会无效,以白名单为准。默认没有白名单设置。

com.ql.util.express.example.MultiLevelSecurityTest#blockWhiteListControlTest

// 必须将该选项设置为 true
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
// 有白名单设置时, 则黑名单失效
QLExpressRunStrategy.addSecureMethod(RiskBean.class, "secureMethod");
// 白名单中的方法, 允许正常调用
expressRunner.execute("import com.ql.util.express.example.RiskBean;" +
                      "RiskBean.secureMethod()", context, null, true, false);
try {
    // java.lang.String.length 不在白名单中, 不允许调用
    expressRunner.execute("'abcd'.length()", context,
                          null, true, false);
    fail("没有捕获到不安全的方法");
} catch (Exception e) {
    assertTrue(e.getCause() instanceof QLSecurityRiskException);
}

// setSecureMethods 设置方式
Set<String> secureMethods = new HashSet<>();
secureMethods.add("java.lang.String.length");
secureMethods.add("java.lang.Integer.valueOf");
QLExpressRunStrategy.setSecureMethods(secureMethods);
// 白名单中的方法, 允许正常调用
Object res = expressRunner.execute("Integer.valueOf('abcd'.length())", context,
                                   null, true, false);
assertEquals(4, res);
try {
    // java.lang.Long.valueOf 不在白名单中, 不允许调用
    expressRunner.execute("Long.valueOf('abcd'.length())", context,
                          null, true, false);
    fail("没有捕获到不安全的方法");
} catch (Exception e) {
    assertTrue(e.getCause() instanceof QLSecurityRiskException);
}

从上图中可以看出白名单有两种设置方式:

  • 添加:QLExpressRunStrategy.addSecureMethod
  • 置换:QLExpressRunStrategy.setSecureMethods

在应用中使用的时,推荐将白名单配置在诸如 etcd,configServer 等配置服务中,根据需求随时调整。

(3)沙箱模式

如果你厌烦上述复杂的配置,只是想完全关闭 QLExpress 和 Java 应用的自由交互,那么推荐使用沙箱模式。

在沙箱模式中,脚本不可以

  • import Java 类
  • 显式引用 Java 类,比如 String a = 'mmm'
  • 取 Java 类中的字段:a = new Integer(11); a.value
  • 调用 Java 类中的方法:Math.abs(12)

脚本可以

  • 使用 QLExpress 的自定义操作符/宏/函数,以此实现与应用的受控交互
  • 使用 . 操作符获取 Mapkey 对应的 value,比如 a 在应用传入的表达式中是一个 Map,那么可以通过 a.b 获取
  • 所有不涉及应用 Java 类的操作

com.ql.util.express.example.MultiLevelSecurityTest#sandboxModeTest

// 开启沙箱模式
QLExpressRunStrategy.setSandBoxMode(true);
ExpressRunner expressRunner = new ExpressRunner();
// 沙箱模式下不支持 import 语句
assertFalse(expressRunner.checkSyntax("import com.ql.util.express.example.RiskBean;"));
// 沙箱模式下不支持显式的类型引用
assertFalse(expressRunner.checkSyntax("String a = 'abc'"));
assertTrue(expressRunner.checkSyntax("a = 'abc'"));
// 无法用 . 获取 Java 类属性或者 Java 类方法
try {
    expressRunner.execute("'abc'.length()", new DefaultContext<>(),
                          null, false, true);
    fail();
} catch (QLException e) {
    // 没有找到方法:length
}
try {
    DefaultContext<String, Object> context = new DefaultContext<>();
    context.put("test", new CustBean(12));
    expressRunner.execute("test.id", context,
                          null, false, true);
    fail();
} catch (RuntimeException e) {
    // 无法获取属性:id
}

// 沙箱模式下可以使用 自定义操作符/宏/函数 和应用进行交互
expressRunner.addFunction("add", new Operator() {
    @Override
    public Object executeInner(Object[] list) throws Exception {
        return (Integer) list[0] + (Integer) list[1];
    }
});
assertEquals(3, expressRunner.execute("add(1,2)", new DefaultContext<>(),
                                      null, false, true));
// 可以用 . 获取 map 的属性
DefaultContext<String, Object> context = new DefaultContext<>();
HashMap<Object, Object> testMap = new HashMap<>();
testMap.put("a", "t");
context.put("test", testMap);
assertEquals("t", expressRunner.execute("test.a", context,
                                        null, false, true));

在沙箱模式下,为了进一步保障内存的安全,建议同时限制脚本能够申请的最大数组长度以及超时时间,设置方法如下:

com.ql.util.express.test.ArrayLenCheckTest

// 限制最大申请数组长度为10, 默认没有限制
QLExpressRunStrategy.setMaxArrLength(10);
ExpressRunner runner = new ExpressRunner();
String code = "byte[] a = new byte[11];";
try {
    // 20ms 超时时间
    runner.execute(code, new DefaultContext<>(), null, false, false, 20);
    Assert.fail();
} catch (QLException e) {
}

QLExpressRunStrategy.setMaxArrLength(-1);
// 20ms 超时时间
runner.execute(code, new DefaultContext<>(), null, false, false, 20);

附录: 版本更新列表

links for us

qlexpress's People

Contributors

alibaba-oss avatar baoxingjie avatar dqinyuan avatar garyapple avatar gitter-badger avatar human-user avatar pymbupt avatar realize096 avatar taokan avatar tiandankanfeng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

qlexpress's Issues

Expression Language Injection Security Risk

Just like spring el\ognl\jstl,alibba QLexpress alse allow programmer to directly call the Java Object to execute the method using the java reflection mechanism,
image
but there is no any security restrictions that rise a security risk while editing rules, just like that poc:
image
so,that cause a remote code execution;
Although it is a feature, there are security risks in design.
fix:

  1. Use java.lang.SecurityManager to protected JVM;
  2. Prevent against by blacklist(Runtime\ ScriptEngine\FileAccess...);
  3. Create a property setting that allows access to java or not.
    see more at:
    https://wooyun.js.org/drops/%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95%E2%80%94%E2%80%94Java-Web-Expression-Language-Injection.html
    https://www.owasp.org/index.php/Expression_Language_Injection
    http://danamodio.com/tag/expression-language-injection/
    https://www.owasp.org/images/7/7e/Owasp_SSTI_final.pdf
    @baoxingjie

有个很想不明白的问题

如图:
image

既然可以通过context传参,为什么还要在express那里填写参数,我觉得这样多次一举了,因为我现在遇到一个需求,我通过不定参数来做到表达式通用化的需求,但如果这样,就会破坏了通用性,而且,我没理解将function添加到runner里面有多方便,本来就是一个方法,直接调用不就得了,为什么调用时还要通过runner再一层包装呢?意义在哪里?

绑定java类,不能注入bean

我想在绑定的java类中,注入StringRedisTemplate,但是无法注入
@component
public class BeanExample {

@Autowired
private StringRedisTemplate stringRedisTemplate;


public boolean redisTest(String key) {
    stringRedisTemplate.opsForValue().set(key, "shfskfhoifhjls");
    return stringRedisTemplate.hasKey(key);
}

},我想注入StringRedisTemplate,这样做可以吗

数组类型推断有问题

@Test
public void testFunction2() throws Exception {
    ExpressRunner runner = new ExpressRunner();
    String exp = "System.out.println(args[0]);";
    String[] args = {"123","456"};
    IExpressContext<String, Object> context = new DefaultContext<String, Object>();
    ((DefaultContext<String, Object>) context).put("args", args);
    Object result = runner.execute(exp,context,null,false,false);
}

发现上面这段代码在实际运行时调用的方法是
public void java.io.PrintStream.println(java.lang.Object)
却不是
public void java.io.PrintStream.println(java.lang.String)
debug时发现findMethod的type是class [Ljava.lang.String;
请问这是一个bug还是开发时特意保留的特性?

附debug到com.ql.util.express.ExpressUtil#findMethod的debugger截图
image

有没有规则检查工具

你好,规则执行报错:
程序错误,不满足语法规范,没有匹配到合适的语法,最大匹配致[0:0]

报这种错感觉很难排查规则哪里写的不对。
各位在排查这种问题的方法是什么,有没有工具支持呢

脚本中使用spring注入报错

Time:23 ms - ID:com.midea.dataexporter.demo.mapper.UserMapper.selectList
Execute SQL:SELECT test_id AS id,name,age,test_type,test_date,role,phone,tenant_id FROM user

java.lang.RuntimeException: 没有实现方法:getObjectInner

at com.ql.util.express.instruction.opdata.OperateDataKeyValue.getObjectInner(OperateDataKeyValue.java:38)

java.lang.NoClassDefFoundError

启动的时候报错:
Throwable Occurs during initialization! java.lang.NoClassDefFoundError: org/apache/commons/beanutils/BeanIntrospector#012 at java.lang.Class.forName0(Native Method)#12 at java.lang.Class.forName(Class.java:264)#12 at com.sun.proxy.$Proxy21.(Unknown Source)#12 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)#12 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)#12 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)#12 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)#12 at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:739)#12 at org.apache.commons.configuration2.builder.fluent.Parameters.createParametersProxy(Parameters.java:307)#12 at org.apache.commons.configuration2.builder.fluent.Parameters.fileBased(Parameters.java:186)#12 at org.apache.commons.configuration2.builder.fluent.Configurations.fileParams(Configurations.java:602)#12 at org.apache.commons.configuration2.builder.fluent.Configurations.fileParams(Configurations.java:614)#12 at org.apache.commons.configuration2.builder.fluent.Configurations.fileBasedBuilder(Configurations.java:132)#12 at org.apache.commons.configuration2.builder.fluent.Configurations.propertiesBuilder(Configurations.java:238)#12 at org.apache.commons.configuration2.builder.fluent.Configurations.properties(Configurations.java:282)#12 at me.ele.config.properties.PropertiesFetcher.buildPropertiesMap(PropertiesFetcher.java:257)#12 at me.ele.config.properties.PropertiesFetcher.buildDevFileMap(PropertiesFetcher.java:206)#12 at me.ele.config.properties.DevProperties.(DevProperties.java:18)#12 at me.ele.config.properties.DevProperties.(DevProperties.java:12)#12 at me.ele.config.properties.PropertiesFetcher.buildEnvHoldKey(PropertiesFetcher.java:160)#12 at me.ele.config.properties.PropertiesFetcher.buildSystemHoldMap(PropertiesFetcher.java:110)#12 at me.ele.config.properties.EnvProperties.(EnvProperties.java:14)#12 at me.ele.config.properties.EnvProperties.(EnvProperties.java:11)#12 at me.ele.config.ConfigFactory.(ConfigFactory.java:25)#12 at me.ele.pylon.core.PylonCore.lambda$new$0(PylonCore.java:97)#12 at me.ele.pylon.core.PylonCore.execute(PylonCore.java:294)#12 at me.ele.pylon.core.PylonCore.(PylonCore.java:94)#12 at me.ele.pylon.core.PylonCore.(PylonCore.java:63)#12 at me.ele.pylon.spring.extension.SOAContainerInitializer.postProcessBeanFactory(SOAContainerInitializer.java:37)#12 at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:284)#12 at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:174)#12 at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:678)#12 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:520)#12 at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:125)#12 at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)#12 at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:109)#12 at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:261)#12 at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)#12 at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)#12 at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)#12 at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:117)#12 at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)#12 at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:228)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:230)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)#12 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:249)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)#12 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)#12 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)#12 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)#12 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)#12 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)#12 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)#12 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)#12 at org.junit.runners.ParentRunner.run(ParentRunner.java:300)#12 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)#12 at org.junit.runner.JUnitCore.run(JUnitCore.java:157)#12 at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)#12 at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)#12 at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)#12 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)#012Caused by: java.lang.ClassNotFoundException: org.apache.commons.beanutils.BeanIntrospector#012 at java.net.URLClassLoader.findClass(URLClassLoader.java:381)#12 at java.lang.ClassLoader.loadClass(ClassLoader.java:424)#12 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)#12 at java.lang.ClassLoader.loadClass(ClassLoader.java:357)#12 ... 63 common frames omitted#012
2018-12-28 17:59:34 INFO [main]: Going to exit with status(-1)!

Process finished with exit code 255

区间表达式执行不支持

现在发现个问题,区间比较表达式,比如1<=a<=100,目前是不支持的,在执行的过程中会优先将1<=a得到的bool值和<=100做比较,这个时候就会抛出异常;

虽然使用1<=a && a<=100也可以兼容下,但是感觉这样还是不方便。请问下能否支持类似1<=a<=100这种表达式吗?

image

内存泄漏问题

5: 2692930 64630320 com.ql.util.express.OperateData
6: 2692720 64625280 com.ql.util.express.instruction.detail.InstructionConstData
存在大量未回收对象,请问如何可以避免呢

请问支持lambda表达式和匿名函数吗?

return data.stream().filter(d -> d.getKey().equals("code") && Integer.parseInt(d.getValue()) > 200).count();
OR
return data.stream().filter(new Predicate() {
@OverRide
public boolean test(TabMonitorData tabMonitorData) {
return tabMonitorData.getKey().equals("code") && Integer.parseInt(tabMonitorData.getValue()) > 200;
}
}).count();

发现一个bug

6.7 - 100 > 39.6 ? 5 == 5 ? 4+5:6-1 : !(100%3-39.0<27) ? 8*2-199: 100%3
这个表达式在3.2.0和3.2.2都无法执行
image

java本身或者其他表达式引擎都是正常的

maven **库没有依赖

com.taobao.util taobao-express 3.0.17 maven **库并没有相应的依赖,这个有计划增加吗?

组合对比有bug

例1
表达式为7<month &&month < 12
key为month
value为9
结果为false

例2
表达式为22<month
key为month
value为100
结果为false

例3
表达式为month < 89
key为month
value为9
结果为false

关于if嵌套if语句的疑问!?

您好,大神:
最近在用贵公司的ql,可是发现if里面无法嵌套if语句,比如if(if 1==2 then false else true) then 1 else 2
这种语句无法执行,程序会报:程序错误,不满足语法规范。
请问一下支持类似这种语法吗?已加您微信了,不知道您收到,谢谢大神。

web可视化

有没有可能实现将配置规则web可视化的打算?或者将配置后的规则链表以树的形式展现

宏定义设置macroName的问题

runner.addMacro方法参数‘macroName’如果前缀是数字的话,会调用Integer.parseInt转换,导致报NumberFormatException异常,比如:'50元立减券',就会出现上述问题。

能增加个精确错误定位提示吗?

目前的错误提示是这样的:java.lang.Exception: 程序错误,不满足语法规范,没有匹配到合适的语法,最大匹配致[0:2]
没有将分词结果暴露出来,具体错误提示在前端没法显示,不利于快速定位问题

Nested parenthesis not supported or a bug?

version : 3.2.0


for the following program:

runner.addOperatorWithAlias("如果", "if",null);
runner.addOperatorWithAlias("则", "then",null);
runner.addOperatorWithAlias("否则", "else",null);

exp = "如果  (如果 1==2 则 false 否则 true) 则 {2+2;} 否则 {20 + 20;}";
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.execute(exp,nil,null,false,false,null);

image

step by step debuging, found that the following function does parse not well

private  static QLMatchResult findMatchStatementWithAddRoot(INodeTypeManager aManager,QLPatternNode pattern ,List<? extends IDataNode> nodes,int point,boolean isRoot,AtomicLong maxMatchPoint) throws Exception{
		QLMatchResult result = null;
		List<QLMatchResultTree> tempList = null;
		int count = 0;
		int lastPoint = point;
		while(true){
			QLMatchResult tempResult = null;
			if (pattern.matchMode == MatchMode.DETAIL) {
				tempResult = matchDetailOneTime(aManager,pattern,nodes, lastPoint,maxMatchPoint);
			}else if (pattern.matchMode == MatchMode.AND) {
				tempResult = matchAndOneTime(aManager,pattern,nodes, lastPoint,maxMatchPoint);
			}else if (pattern.matchMode == MatchMode.OR) {
				tempResult = matchOrOneTime(aManager,pattern,nodes, lastPoint,maxMatchPoint);
			}else{
				throw new Exception("不正确的类型:" + pattern.matchMode.toString());
			}
			if(tempResult == null){
				if(count >= pattern.minMatchNum && count <=pattern.maxMatchNum){
					//正确匹配
					if(tempList == null){
						 tempList = new ArrayList<QLMatchResultTree>();
					}
					result = new QLMatchResult(tempList,lastPoint);
				}else{
					result = null;
				}
				break;
			}else{
				if(tempList == null){
					 tempList = new ArrayList<QLMatchResultTree>();
				}
				lastPoint = tempResult.matchLastIndex;
				if(pattern.isTreeRoot == true){
					if(tempResult.matchs.size() > 1){
						throw new Exception("根节点的数量必须是1");
					}
					if(tempList.size() == 0){
						tempList.addAll(tempResult.matchs);
					}else{	
						tempResult.matchs.get(0).addLeftAll(tempList);
						tempList = tempResult.matchs;
					}
				}else{
				   tempList.addAll(tempResult.matchs);
				}
			}
			count = count + 1;			
			if(count == pattern.maxMatchNum){
				result = new QLMatchResult(tempList,lastPoint);
				break;
			}
		}
		if(result != null && pattern.isSkip == true){
			//忽略跳过所有匹配到的节点
			result.matchs.clear();
		}

		if(result != null && result.matchs.size() >0 && pattern.rootNodeType != null){
			QLMatchResultTree tempTree = new QLMatchResultTree(pattern.rootNodeType,nodes.get(0).createExpressNode(pattern.rootNodeType,null));
			tempTree.addLeftAll(result.matchs);
			result.matchs.clear();
			result.matchs.add(tempTree);
		}
		return result;
	}

发现了个bug

public static void main(String[] args) throws Exception {
        ExpressRunner runner = new ExpressRunner();
        DefaultContext<String, Object> context = new DefaultContext<String, Object>();
        String express = "part = \"1@2@3\".split(\"@\");\n" +
                "Integer.valueOf(part[2]);";
        runner.execute(express, context, null, true, false);
}

执行这段代码会报这个错误

Caused by: java.lang.Exception: 没有找到java.lang.Class的方法:valueOf([Ljava.lang.String;)
	at com.ql.util.express.instruction.op.OperatorMethod.executeInner(OperatorMethod.java:96)
	at com.ql.util.express.instruction.op.OperatorBase.execute(OperatorBase.java:67)
	at com.ql.util.express.instruction.detail.InstructionOperator.execute(InstructionOperator.java:44)
	... 8 more

把express的内容改为

part = "1@2@3".split("@");
str = part[2];
Integer.valueOf(str);

就可以了

QLExpress 脚本中异常捕获问题

请问,能在脚本中进行异常捕获吗?当业务逻辑复杂时,需要在脚本中对出现的异常进行捕获,进而进行补偿操作,QLExpress是否支持这种功能?

可以建一个群?

我通过macro调用function,报找不到function

 ExpressRunner runner = new ExpressRunner(false, false);
          runner.addFunctionOfServiceMethod("changeBrand",new BrandService(),"changeBrand",new Class[] {DroolsRequest.class},null);
             runner.addMacro("changeBrandMacro","changeBrand11(droolsRequest)");
            DefaultContext<String, Object> context = new DefaultContext<>();
            context.put("PARAM1", 12);
            context.put("PARAM2", 60);
            context.put("droolsRequest",droolsRequest);
            expression = initial(runner, “changeBrandMacro and PARAM1 + PARAM2>60”);
            result = (Boolean) runner.execute(expression, context, errorInfo, true, false);

其中

public class BrandService {

    public boolean changeBrand(DroolsRequest droolsRequest){
        System.out.println("++++++++++++++++++++++++++++++++++++++++++++++");
        System.out.println(droolsRequest.asXML());
        return true;
    }
}

报错
java.lang.Exception: 在Runner的操作符定义和自定义函数中都没有找到"changeBrand"的定义
我看了下源码,macro中去调用function时候,查找的function是去symbolTable找的,但是addFunctionOfServiceMethod却是往functions里边添加的,请问是我什么地方写错了么?

是否支持操作符< <= > >=当 对象为空的时候返回false

OperatorEqualsLessMore 类
if (obj1 == null && obj2 == null) {
if (opStr.equals("==")) {
return true;
} else if (opStr.equals("!=") || opStr.equals("<>")) {
return false;
} else {
throw new Exception("两个空操作数不能执行这个操作:" + opStr);
}
} else if (obj1 == null || obj2 == null) {
if (opStr.equals("==")) {
return false;
} else if (opStr.equals("!=") || opStr.equals("<>")) {
return true;
} else {
throw new Exception("空操作数不能执行这个操作:" + opStr);
}
}

是否考虑List类型数据支持数组操作?

例:

        JSONObject json = new JSONObject();
        json.put("attr", Lists.newArrayList("1", "2", "3"));

        DefaultContext<String, Object> properties = new DefaultContext<String, Object>();
        properties.put("json", json);

        String ex = "json.attr.get(1)";
        Object r = runner.execute(ex, properties, null, true, false);
        System.out.println(r);

        //是否可以支持下面的操作?
//        ex = "json.attr[1]";
//        r = runner.execute(ex, properties, null, true, false);
//        System.out.println(r);

表达式计算不支持单字符类型比较

例如,如下测试:

@Test
public void testCharcharComapre() throws Exception {
	ExpressRunner runner = new ExpressRunner();
	String express = "'a'=='a' && '9.2'>='9.2' && '2018-05-09 17:47:31'>='2018-05-09 17:47:30'";
	Boolean r = (Boolean) runner.execute(express, null, null, true, false);
	assert r == true;
}

执行报以下错误:

java.lang.Exception: run QlExpress Exception at line 1 :

	at com.ql.util.express.instruction.detail.InstructionOperator.execute(InstructionOperator.java:48)
	at com.ql.util.express.InstructionSet.executeInnerOrigiInstruction(InstructionSet.java:204)
	at com.ql.util.express.InstructionSet.excute(InstructionSet.java:170)
	at com.ql.util.express.InstructionSetRunner.execute(InstructionSetRunner.java:59)
	at com.ql.util.express.InstructionSetRunner.execute(InstructionSetRunner.java:46)
	at com.ql.util.express.InstructionSetRunner.executeOuter(InstructionSetRunner.java:18)
	at com.ql.util.express.ExpressRunner.execute(ExpressRunner.java:570)
	at com.ql.util.express.ExpressRunner.execute(ExpressRunner.java:538)
	at com.ql.util.express.example.OperatorTest.testCharcharComapre(OperatorTest.java:93)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
	at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
	at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
	at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
	at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
	at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
	at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
	at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
	at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
	at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
	at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
	at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:130)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.Exception: aa不能执行compare 操作
	at com.ql.util.express.Operator.compareData(Operator.java:66)
	at com.ql.util.express.instruction.op.OperatorEqualsLessMore.executeInner(OperatorEqualsLessMore.java:50)
	at com.ql.util.express.instruction.op.OperatorEqualsLessMore.executeInner(OperatorEqualsLessMore.java:25)
	at com.ql.util.express.instruction.op.OperatorEqualsLessMore.executeInner(OperatorEqualsLessMore.java:21)
	at com.ql.util.express.Operator.executeInner(Operator.java:20)
	at com.ql.util.express.instruction.op.OperatorBase.execute(OperatorBase.java:67)
	at com.ql.util.express.instruction.detail.InstructionOperator.execute(InstructionOperator.java:44)
	... 29 more

不支持注释吗?

加了注释(//)报语法错误,去掉就OK了
是不支持还是我用法不正确?

针对[return -50],如果return后面跟一个负数值时程序解析错误

@Test
public void testChange_1() throws Exception {
    ExpressRunner runner = RuleEngine.newInstance();
    IExpressContext<String, Object> context = new DefaultContext<String, Object>();
    context.put("test", 2.004);
    String test = "if(test > 0){return 50}";
    System.out.println(runner.execute(test, context, null, true, false));

    String test1 = "if(test > 0){return -50}";
    System.out.println(runner.execute(test1, context, null, true, false));
}

从测试用例得出test1执行失败,具体原因应该是在ExpressParse.transferWord2ExpressNode 对'-'这种特殊情况考虑不够。

针对数字开头的上下文key支持有问题

下面的测试用例执行会解析不了上下文的值
@test
public void testArithmetic_map() throws Exception {
ExpressRunner runner = new ExpressRunner(true, true);
runner.addMacro("计算平均成绩","(90天浏览时长 + 数学 + 英语)/3");
IExpressContext<String, Object> context =new DefaultContext<String, Object>();
context.put("90天浏览时长", 88);
context.put("数学", 99);
context.put("英语", 95);
System.out.println(runner.execute("计算平均成绩", context, null, true, false));
}

[90天浏览时长]这样的key读取会有问题,具体的错误逻辑应该在ExpressParse.class中的transferWord2ExpressNode,针对数字开头的key没有考虑上述的特殊情况。
不知道还能不能更新啦改项目,我这边自己打个分支先改造了把

属性访问无法支持Builder模式的GetSet方法

运行环境

JDK 1.8
QLExpress 3.2.2
Windows 10

问题描述

当使用Builder模式的属性时, 系统无法正确读取属性值.

示例代码

public class ELDemoEntity {
    private String name;
    private Long id;
    public String getName() {
        return name;
    }
    public ELDemoEntity setName(String name) {
        this.name = name;
        return this;
    }
    public Long getId() {
        return id;
    }
    public ELDemoEntity setId(Long id) {
        this.id = id;
        return this;
    }
}

@Test
    public void test5() throws Exception {
        ExpressRunner runner = new ExpressRunner(true,true);
        String express = "if(demo.getId()==1)then{demo.id=2};return 0";
        System.out.println(express);

        IExpressContext<String,Object> expressContext = new DefaultContext<String,Object>();
        ELDemoEntity entity = new ELDemoEntity();
        entity.setId(1L).setName("zhangsan");
        expressContext.put("demo", entity);
        Object r = runner.execute(express,
                expressContext, null, false, false);
        System.out.println("r=" + r);
        System.out.println(entity.getId());
    }

错误的堆栈信息

java.lang.Exception: run QlExpress Exception at line 1 :

	at com.ql.util.express.instruction.detail.InstructionOperator.execute(InstructionOperator.java:48)
	at com.ql.util.express.InstructionSet.executeInnerOrigiInstruction(InstructionSet.java:217)
	at com.ql.util.express.InstructionSet.excute(InstructionSet.java:183)
	at com.ql.util.express.InstructionSetRunner.execute(InstructionSetRunner.java:59)
	at com.ql.util.express.InstructionSetRunner.execute(InstructionSetRunner.java:46)
	at com.ql.util.express.InstructionSetRunner.executeOuter(InstructionSetRunner.java:18)
	at com.ql.util.express.ExpressRunner.execute(ExpressRunner.java:587)
	at com.ql.util.express.ExpressRunner.execute(ExpressRunner.java:555)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
...
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
Caused by: java.lang.RuntimeException: 不能访问cn.loopcloud.ELDemoEntity@59d4cd39的property:id
	at com.ql.util.express.ExpressUtil.setProperty(ExpressUtil.java:587)
	at com.ql.util.express.instruction.opdata.OperateDataField.setObject(OperateDataField.java:108)
	at com.ql.util.express.instruction.op.OperatorEvaluate.executeInner(OperatorEvaluate.java:37)
	at com.ql.util.express.instruction.op.OperatorEvaluate.executeInner(OperatorEvaluate.java:18)
	at com.ql.util.express.instruction.op.OperatorBase.execute(OperatorBase.java:67)
	at com.ql.util.express.instruction.detail.InstructionOperator.execute(InstructionOperator.java:44)
	... 30 more
Caused by: java.lang.NoSuchMethodException: Property 'id' has no setter method in class 'class cn.loopcloud.ELDemoEntity'
	at org.apache.commons.beanutils.PropertyUtilsBean.setSimpleProperty(PropertyUtilsBean.java:2084)
	at org.apache.commons.beanutils.PropertyUtilsBean.setNestedProperty(PropertyUtilsBean.java:1903)
	at org.apache.commons.beanutils.PropertyUtilsBean.setProperty(PropertyUtilsBean.java:2010)
	at org.apache.commons.beanutils.PropertyUtils.setProperty(PropertyUtils.java:896)
	at com.ql.util.express.ExpressUtil.setProperty(ExpressUtil.java:584)
	... 35 more

建议方案

  1. 能否将对象属性, 字段, 方法的操作抽象为接口并支持外部自定义, 这样, 能很方便的支持外部切换对象访问逻辑.
  2. 是否有计划提供比apache beanutils更加高效的属性访问方式, 如字节码访问等, 以提升性能

InstructionFactory 缓存

InstructionFactory的Map<String,InstructionFactory> instructionFactory 只看到有get , 没看到那里有put

public static InstructionFactory getInstructionFactory(String factory) {
	try {
		InstructionFactory result = instructionFactory.get(factory);
		if (result == null) {
			result = (InstructionFactory) Class.forName(factory)
					.newInstance();
		}
		return result;

//返回结果 [1, 2]

//(3)addFunction
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addFunction("join",new JoinOperator());
Object r = runner.execute("join(1,2,3)", context, null, false, true);
System.out.println(r);
//返回结果 [1, 2, 3]
结果真的是[1,2,3]吗

17:26:37.341 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 1
17:26:37.341 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 2
17:26:37.341 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 3
17:26:37.341 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - join(1,2,3)
[1, 2]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.