QLExpres_之_基本语法
前言
Github:https://github.com/HealerJean
1、QlExpress
的基本规则语法
1.1、普通 java
语法
⬤ 支持
+
,-
,*
,/
,<
,>
,<=
,>=
,==
,!=
,<>
【等同于!=
】,%
,mod
【取模等同于%
】,++
,--
,⬤
in
【类似sql
】,like
【sql
语法】,&&
,||
,!
,等操作符⬤ 支持
for
,break
、continue
、if
then
else
等标准的程序控制逻辑
@Test
public void test() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
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, true);
System.out.println(r);
}
1.2、脚本中定义 function
1.2.1、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);
1.2.2、Test
@Test
public void test_functionStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
String functionStatement = "function add(int a,int b){\n" +
" return a+b;\n" +
"};\n" +
"\n" +
"function sub(int a,int b){\n" +
" \n" +
" return a - b;\n" +
"};\n" +
"\n" +
"a=10;\n" +
"return add(a,4) + sub(a,9);";
Object result = runner.execute(functionStatement, new DefaultContext<>(), null, false, false);
System.out.println(result);
// 10
}
1.3、对 Java
对象的操作
系统自动会
import java.lang.*
,import java.util.*;
@Test
public void test_objectStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
// TradeEvent tradeEvent = new TradeEvent();
// tradeEvent.setPrice(20.0);
// tradeEvent.setName("购物");
// tradeEvent.setId(UUID.randomUUID().toString());//
//
String objectStatement = "import me.aihe.demo.trade.TradeEvent;\n" +
" tradeEvent = new TradeEvent();\n" +
" tradeEvent.setPrice(20.0);\n" +
" tradeEvent.id=UUID.randomUUID().toString();\n" +
" System.out.println(tradeEvent.getId());\n" +
" System.out.println(tradeEvent.price);";
runner.execute(objectStatement, new DefaultContext<>(), null, false, false);
}
1.4、扩展操作符:Operator
1.4.1、替换 if
then
else
等关键字
@Test
public void test_extendOperatorStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("则", "then", null);
runner.addOperatorWithAlias("否则", "else", null);
IExpressContext<String, Object> context = new DefaultContext<>();
context.put("语文", 88);
context.put("数学", 99);
context.put("英语", 95);
String exp = "如果 (语文 + 数学 + 英语 > 270) 则 {return 1;} 否则 {return 0;}";
Object result = runner.execute(exp, context, null, false, false, null);
System.out.println(result);
//1
}
1.4.2、自定义操作符
public class JoinOperator extends Operator {
public Object executeInner(Object[] list) throws Exception {
java.util.List result = new java.util.ArrayList();
Object opdata1 = list[0];
if (opdata1 instanceof java.util.List) {
result.addAll((java.util.List) opdata1);
} else {
result.add(opdata1);
}
for (int i = 1; i < list.length; i++) {
result.add(list[i]);
}
return result;
}
}
1.4.2.1、addOperator
添加操作符
@Test
public void test_add_operator() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "1 join 2 join 3";
runner.addOperator("join", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
// [1, 2, 3]
}
1.4.2.2、replaceOperator
替换操作符
替换原有的操作符,比如将
+
替换为join
@Test
public void test_replace_operator() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "1 + 2 + 3";
runner.replaceOperator("+", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
// [1, 2, 3]
}
1.4.2.3、addFunction
添加方法
@Test
public void test_add_function() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "join(1,2,3)";
runner.addFunction("join", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
// [1, 2, 3]
}
暂时没看懂
@Test
public void test_customOperatorStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
class JoinOperator extends Operator {
@Override
public Object executeInner(Object[] list) throws Exception {
Object opdata1 = list[0];
Object opdata2 = list[1];
if (opdata1 instanceof java.util.List) {
((java.util.List) opdata1).add(opdata2);
return opdata1;
} else {
java.util.List result = new java.util.ArrayList();
result.add(opdata1);
result.add(opdata2);
return result;
}
}
}
// 返回结果 [1, 2, 3]
//(1)addOperator 添加操作符
DefaultContext<String, Object> context = new DefaultContext<>();
runner.addOperator("join", new JoinOperator());
Object r = runner.execute("1 join 2 join 3", context, null, false, false);
System.out.println(r);
// (2) addFunction 添加方法
// 返回结果:[1 , 2 ]
runner.addFunction("joinfunc", new JoinOperator());
r = runner.execute("joinfunc(1,2,3)", context, null, false, false);
System.out.println(r);
//(3)replaceOperator 替换操作符
// 返回结果 [1, 2, 3]
runner.replaceOperator("+", new JoinOperator());
r = runner.execute("1 + 2 + 3", context, null, false, false);
System.out.println(r);
}
1.5、绑定 java
类或者对象的method
@Test
public void test_workWithJavaStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
// 1、在使用的时候会创建对象
runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs", new String[]{"double"}, null);
// 2、对象已经存在,直接调用对象中的方法
runner.addFunctionOfServiceMethod("打印", System.out, "println", new String[]{"String"}, null);
String exp = "a = 取绝对值(-100); 打印(\"Hello World\"); 打印(a.toString())";
DefaultContext<String, Object> context = new DefaultContext<>();
runner.execute(exp, context, null, false, false);
System.out.println(context);
}
1.6、macro
宏定义
即预先定义一些内容,在使用的时候直接替换
Macro
中的变量为上下文的内容
@Test
public void test_macronStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
//没有顺序限制
runner.addMacro("是否优秀", "计算平均成绩 > 90");
runner.addMacro("计算平均成绩", "(语文 + 数学 + 英语 ) /3.0");
IExpressContext<String, Object> context = new DefaultContext<>();
context.put("语文", 88);
context.put("数学", 99);
context.put("英语", 95);
Object result = runner.execute("是否优秀", context, null, false, false);
System.out.println(result);
}
1.7、编译脚本,查询外部需要定义的变量和函数。
@Test
public void test_getOutVarNames() throws Exception {
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 : 语文
}
1.8、集合的快速写法
@Test
public void test_fast() throws Exception {
ExpressRunner runner = new ExpressRunner(false,false);
DefaultContext<String, Object> context = new DefaultContext<>();
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);
//3
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);
//5
express = "abc = [1,2,3]; return abc[1]+abc[2];";
r = runner.execute(express, context, null, false, false);
System.out.println(r);
//5
}
1.9、集合的遍历
1.9.1、遍历 Map
1.9.1.1、Test
keySet = map.keySet();
objArr = keySet.toArray();
for (i=0;i<objArr.length;i++) {
key = objArr[i];
System.out.println(map.get(key));
}
@Test
public void test_collectionStatement() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> defaultContext = new DefaultContext<>();
HashMap<String, String> mapData = new HashMap() ;
defaultContext.put("map", mapData);
//ql不支持for(obj:list){}的语法,只能通过下标访问。
String mapTraverseStatement = " keySet = map.keySet();\n" +
" objArr = keySet.toArray();\n" +
" for (i=0;i<objArr.length;i++) {\n" +
" key = objArr[i];\n" +
" System.out.println(map.get(key));\n" +
" }";
runner.execute(mapTraverseStatement, defaultContext, null, false, false);
System.out.println(defaultContext);
// hello
// world
// !@#$
// {i=3, objArr=[Ljava.lang.Object;@6b419da, map={a=hello, b=world, c=!@#$}, keySet=[a, b, c], key=c}
}
2、运行参数和 api
介绍
2.1、属性开关
2.1.1、isPrecise
:是否需要高精度计算
高精度计算在会计财务中非常重要,
java
的float
、double
、int
、long
存在很多隐式转换,做四则运算和比较的时候其实存在非常多的安全隐患。 所以类似汇金的系统中,会有很多BigDecimal
转换代码。而使用QLExpress
,你只要关注数学公式本身 订单总价 = 单价 * 数量 + 首重价格 + ( 总重量 - 首重) * 续重单价 ,然后设置这个属性即可,所有的中间运算过程都会保证不丢失精度。
@Test
public void is_precise() throws Exception {
// 一个参数是是否开启高精度默认false,第二个参数是是否开启trace。
ExpressRunner runner = new ExpressRunner(false, false);
DefaultContext<String, Object> context = new DefaultContext<>();
//订单总价 = 单价 * 数量 + 首重价格 + (总重量 - 首重) * 续重单价
context.put("单价", 1.25);
context.put("数量", 100);
context.put("首重价格", 125.25);
context.put("总重量", 20.55);
context.put("首重", 10.34);
context.put("续重单价", 333.33);
String express = "单价 * 数量 + 首重价格 + ( 总重量 - 首重 ) / 续重单价";
Object totalPrice = runner.execute(express, context, null, true, false);
System.out.println("totalPrice:" + totalPrice);
}
2.1.2、isShortCicuit
:是否使用逻辑短路特性
在很多业务决策系统中,往往需要对布尔条件表达式进行分析输出,普通的 `java运算一般会通过逻辑短路来减少性能的消耗。例如规则公式: star > 10000 and shopType in (‘tmall’, ‘juhuasuan’) and price between (100, 900) 假设第一个条件 star>10000 不满足就停止运算。但业务系统却还是希望把后面的逻辑都能够运算一遍,并且输出中间过程,保证更快更好的做出决策。
@Test
public void testShortCircuit() throws Exception {
ExpressRunner runner = new ExpressRunner();
runner.setShortCircuit(false);
runner.addOperatorWithAlias("小于", "<", "$1 小于 $2 不满足期望");
runner.addOperatorWithAlias("大于", ">", "$1 大于 $2 不满足期望");
IExpressContext<String, Object> expressContext = new DefaultContext<>();
expressContext.put("违规天数", 100);
expressContext.put("虚假交易扣分", 13);
expressContext.put("VIP", false);
List<String> errorInfo = new ArrayList<>();
String expression = "2 小于 2 and (违规天数 小于 90 or 虚假交易扣分 小于 12)";
boolean result = (Boolean)runner.execute(expression, expressContext, errorInfo, true, false);
if (result) {
System.out.println("result is success!");
} else {
System.out.println("result is fail!");
for (String error : errorInfo) {
System.out.println(error);
}
}
}
runner.setShortCircuit(false);
result is fail!
2 小于 2 不满足期望
违规天数:100 小于 90 不满足期望
虚假交易扣分:13 小于 12 不满足期望
runner.setShortCircuit(true);
result is fail!
2 小于 2 不满足期望
2.1.3、isTrace
:
是否输出所有的跟踪信息,同时还需要
log
级别是DEBUG
级别⬤ 这个主要是是否输出脚本的编译解析过程,一般对于业务系统来说关闭之后会提高性能。
ExpressRnner runner = new ExpressRunner(true, false);//第二个参数是否开启trace
2.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,
Log aLog);
参数 | 说明 |
---|---|
expressString |
程序文本 |
context |
可以扩展为包含ApplicationContext |
errorList |
输出的错误信息 List |
isCache |
是否使用 Cache 中的指令集,建议为 true |
isTrace |
是否输出详细的执行指令信息,建议为 false |
aLog |
输出的 log |
2.3、功能扩展 API
列表
QLExpress
主要通过子类实现Operator.java
提供的以下方法来最简单的操作符定义,然后可以被通过addFunction
或者addOperator
的方式注入到ExpressRunner
中。⬤ 具体参见上面的
JoinOperator
的例子。⬤ 如果你使用
Operator
的基类OperatorBase.java
将获得更强大的能力,基本能够满足所有的要求。
2.3.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.3.2、Operator
相关 API
提到脚本语言的操作符,优先级、运算的目数、覆盖原始的操作符(+,-,*,/等等)都是需要考虑的问题,
QLExpress
统统帮你搞定了。
//添加操作符号,可以设置优先级
void addOperator(String name,Operator op);
void addOperator(String name,String aRefOpername,Operator op);
//替换操作符处理
//比如将 + 替换成自定义的 operator 见上面的例子
OperatorBase replaceOperator(String name,OperatorBase op);
//添加操作符和关键字的别名,比如 if..then..else -> 如果。。那么。。否则。。
//具体见上面的例子
void addOperatorWithAlias(String keyWordName,
String realKeyWordName,
String errorInfo);
2.3.3、宏相关 API
QLExpress
的宏定义比较简单,就是简单的用一个变量替换一段文本,和传统的函数替换有所区别。见上面使用宏计算是否优秀的例子
//比如addMacro("天猫卖家","userDO.userTag &1024 ==1024")
void addMacro(String macroName,String express)
2.3.4、java
class
相关 API
QLExpress
可以通过给java
类增加或者改写一些method
和field
,比如链式调用:"list.join("1").join("2")"
,比如中文属性:”list.长度
“。注意:这些类的字段和方法是执行器通过解析语法执行的,而不是通过字节码增强等技术,所以只在脚本运行期间生效,不会对
jvm
整体的运行产生任何影响,所以是绝对安全的。
//添加类的属性字段
void addClassField(String field, Class<?>bindingClass, Class<?>returnType, Operator op);
//添加类的方法
void addClassMethod(String name, Class<?>bindingClass, OperatorBase op);
2.3.5、语法树解析变量、函数的 API
这些接口主要是对一个脚本内容的静态分析,可以作为上下文创建的依据,也可以用于系统的业务处理。 比如:计算 “a + fun1(a) + fun2(a + b) + c.getName()” 包含的变量: a, b, c 包含的函数: fun1, fun2
//获取一个表达式需要的外部变量名称列表
String[] getOutVarNames(String express);
String[] getOutFunctionNames(String express);
2.3.6、语法解析校验 api
脚本语法是否正确,可以通过
ExpressRunner
编译指令集的接口来完成。
String expressString = "for(i = 0; i < 10; i++) {sum = i + 1;} return sum;";
InstructionSet instructionSet = expressRunner.parseInstructionSet(expressString);
//如果调用过程不出现异常,指令集instructionSet就是可以被加载运行(execute)了!
2.3.7、指令集缓存相关的 api
因为
QLExpress
对文本到指令集做了一个本地HashMap
缓存,通常情况下一个设计合理的应用脚本数量应该是有限的,缓存是安全稳定的,但是也提供了一些接口进行管理。
//优先从本地指令集缓存获取指令集,没有的话生成并且缓存在本地
InstructionSet getInstructionSetFromLocalCache(String expressString);
//清除缓存
void clearExpressCache();
2.3.8、安全风险控制
2.3.8.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);
}
2.3.8.2、防止调用不安全的系统 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);
}
2.3.9、增强上下文参数 Context
相关的 api
上下文参数
IExpressContext
context
非常有用,它允许put
任何变量,然后在脚本中识别出来。在实际中我们很希望能够无缝的集成到spring
框架中,可以仿照下面的例子使用一个子类。
2.3.9.1、与 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);
}
}
2.3.9.2、自定义函数操作符获取原始的 context
控制上下文
自定义的
Operator
需要直接继承OperatorBase
,获取到parent
即可,可以用于在运行一组脚本的时候,直接编辑上下文信息,业务逻辑处理上也非常有用。
public 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);
String message = "name:".concat(name).concat(",")
.concat("key:").concat(key).concat(",")
.concat("value:").concat(value.toString());
System.out.println(message);
return new OperateData(message, String.class);
}
}
/**
* 3、自定义函数操作符获取原始的 `context` 控制上下文
*/
@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<>();
context.put("success", "true");
Object result = runner.execute(express, context, null, false, true);
System.out.println("返回结果:result:".concat(result.toString()));
System.out.println("返回context:context:".concat(result.toString()));
}
2.3.10、多级别安全控制
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 中的类产生交互
2.3.10.1、黑名单控制
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
2、支持通过 QLExpressRunStrategy.addSecurityRiskMethod
额外添加
// 必须将该选项设置为 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.3.10.2、白名单控制
白名单有两种设置方式:
⬤ 添加:
QLExpressRunStrategy.addSecureMethod
⬤ 置换:
QLExpressRunStrategy.setSecureMethods
在应用中使用的时,推荐将白名单配置在诸如 etcd
,configServer
等配置服务中,根据需求随时调整。
1、编译期白名单:编译期白名单是类维度的,脚本中只允许显式引用符合白名单条件的类,支持两种设置方式,精确设置某个类,以及设置某个类的全部子类。
编译期白名单只能检测出脚本编译时能够确认的类型,任何运行时出现的类型都是无法检测的,诸如各种反射Class.forName
, ClassLoader.loadClass
,或者没有声明类型的变量等等,因为编译期白名单只能增加黑客的作案成本,是容易被绕过。因此建议编译期白名单只用来帮助脚本校验,如果需要接收终端用户输入,运行期白名单是务必要配置的。
// 设置编译期白名单
QLExpressRunStrategy.setCompileWhiteClassList(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)"));
2、运行时白名单:
⬤ 如果有白名单设置,所有的黑名单设置就都会无效,以白名单为准。默认没有白名单设置。
// 必须将该选项设置为 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);
}
2.3.10.3、沙箱模式
如果你厌烦上述复杂的配置,只是想完全关闭 QLExpress 和 Java 应用的自由交互,那么推荐使用沙箱模式。
在沙箱模式中,脚本不可以:
⬤ import Java 类
⬤ 显式引用 Java 类,比如
String a = 'mmm'
⬤ 取 Java 类中的字段:
a = new Integer(11); a.value
⬤ 调用 Java 类中的方法:
Math.abs(12)
在沙箱模式中,脚本可以:
⬤ 使用 QLExpress 的自定义操作符/宏/函数,以此实现与应用的受控交互
⬤ 使用
.
操作符获取Map
的key
对应的value
,比如a
在应用传入的表达式中是一个Map
,那么可以通过a.b
获取⬤ 所有不涉及应用 Java 类的操作
// 开启沙箱模式
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));