1. 表达式引擎选型
如果要做一个规则引擎,需要匹配一组规则集来做决策,此时就需要一个表达式引擎来辅助。
选择 5 种表达式引擎进行性能对比,从而选择最优表达式引擎和最优方案。Janino、QLExpress、MEVL、JUEL、FEL。
当然,除了计算性能,还要考虑是否满足功能,以及是否有安全漏洞等,本文仅进行表达式引擎的性能对比。
2. 性能测试
2.1 测试维度
- 表达式维度:主要采用了3种表达式,表达式的语法在不同的表达式引擎中也不同,详见3.2 。
- 表达式1: 最常见的场景(多个条件进行and操作):city.equals(‘杭州’) && age<=20
- 表达式2: 包含特殊的操作(contains):city.equals(‘杭州’) && age<=20 && stringList.contains(str)
- 表达式3: 一个稍微复杂一点的语法树 :a>1 && ((b>1 || c<1) || (a>1 && b<1 && c>1))
- 数据量
- 1000万
- 2000万
- 4000万
2.2 测试方式
根据数据量执行多少次 for 循环,记录下表达式计算的总耗时,单位毫秒 (ms)。为了保证一定的差异性,变量赋值的时候,采用变化的值。代码详见 3.2 。
2.3 结论
- 表达式1:city.equals(“杭州”) && age<=20
Janino | QL | Mvel (预编译) | Juel | Fel | |
---|---|---|---|---|---|
1000万 | 583 | 6885 | 2634 | 3174 | 125065 |
2000万 | 1171 | 11363 | 4167 | 4318 | 277257 |
4000万 | 1951 | 22979 | 8544 | 9117 | 519036 |
- 表达式2: city.equals(“杭州”) && age<=20 && stringList.contains(str)
Janino | QL | Mvel (预编译) | Juel | Fel | |
---|---|---|---|---|---|
1000万 | 508 | 6473 | 2810 | 2787 | 166865 |
2000万 | 983 | 12793 | 4627 | 5728 | 339085 |
4000万 | 2008 | 25595 | 9656 | 11417 | 670728 |
- 表达式3:a>1 && ((b>1 || c<1) || (a>1 && b<1 && c>1))
Janino | QL | Mvel (预编译) | Juel | Fel | |
---|---|---|---|---|---|
1000万 | 489 | 3082 | 2206 | 3056 | 167739 |
2000万 | 947 | 6156 | 3445 | 5950 | 321651 |
4000万 | 1909 | 12353 | 7284 | 12934 | 642368 |
Janino 表达式引擎的性能最优。
处理速度上,Janino > Mvel(预编译) > Juel > QL > Fel 。
3. 附录
3.1 机器配置
本机 Mac 配置:
MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports)
处理器 2.4 GHz 四核Intel Core i5
内存 16 GB 2133 MHz LPDDR3
图形卡 Intel Iris Plus Graphics 655 1536 MB
3.2 Java 测试代码
- Janino
import org.codehaus.commons.compiler.CompilerFactoryFactory;
import org.codehaus.commons.compiler.IExpressionEvaluator;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Description:
*
* @author mwt
* @version 1.0
* @date 2020/5/21
*/
public class JaninoExpressionTest {
@Test
public void test1() throws Exception {
String ruleExpress = "city.equals(\"杭州\") && age<=20";
long count1 = 1000 * 10000;
System.out.println("\nJaninoExpression1 数据量 1000 万:");
execute(count1, ruleExpress);
long count2 = 2000 * 10000;
System.out.println("\nJaninoExpression1 数据量 2000 万:");
execute(count2, ruleExpress);
long count3 = 4000 * 10000;
System.out.println("\nJaninoExpression1 数据量 4000 万:");
execute(count3, ruleExpress);
}
@Test
public void test2() throws Exception {
String ruleExpress = "city.equals(\"杭州\") && age<=20 && stringList.contains(str)";
long count1 = 1000 * 10000;
System.out.println("\nJaninoExpression2 数据量 1000 万:");
execute(count1, ruleExpress);
long count2 = 2000 * 10000;
System.out.println("\nJaninoExpression2 数据量 2000 万:");
execute(count2, ruleExpress);
long count3 = 4000 * 10000;
System.out.println("\nJaninoExpression2 数据量 4000 万:");
execute(count3, ruleExpress);
}
@Test
public void test3() throws Exception {
String ruleExpress = "a>1 && ((b>1 || c<1) || (a>1 && b<1 && c>1))";
long count1 = 1000 * 10000;
System.out.println("\nJaninoExpression3 数据量 1000 万:");
execute(count1, ruleExpress);
long count2 = 2000 * 10000;
System.out.println("\nJaninoExpression3 数据量 2000 万:");
execute(count2, ruleExpress);
long count3 = 4000 * 10000;
System.out.println("\nJaninoExpression3 数据量 4000 万:");
execute(count3, ruleExpress);
}
private void execute(long count, String expression) throws Exception {
IExpressionEvaluator expressionEvaluator =
CompilerFactoryFactory.getDefaultCompilerFactory().newExpressionEvaluator();
expressionEvaluator.setExpressionType(boolean.class);
expressionEvaluator.setParameters(
new String[]{
"city", "age", "stringList", "str", "a", "b", "c"},
new Class[]{
String.class, Integer.class, List.class, String.class,
Integer.class, Integer.class, Integer.class}
);
expressionEvaluator.cook(expression);
long start = System.currentTimeMillis();
List<String> stringList = new ArrayList<String>(2);
stringList.add("hello");
stringList.add("world");
Random random = new Random();
for (int i = 0; i < count; i++) {
Object[] arguments = new Object[7];
int age = random.nextInt(50);
if (i % 2 == 0) {
arguments[0] = "杭州";
} else {
arguments[0] = "北京";
}
arguments[1] = age;
arguments[2] = stringList;
if (i % 3 == 0) {
arguments[3] = "hello";
} else if (i % 3 == 1) {
arguments[3] = "world";
} else {
arguments[3] = "anything";
}
arguments[4] = random.nextInt(2);
arguments[5] = random.nextInt(2);
arguments[6] = random.nextInt(2);
Object res = expressionEvaluator.evaluate(arguments);
}
long end = System.currentTimeMillis();
long intervalInMs = end - start;
float avg = (float) count / intervalInMs * 1000;
System.out.println("总耗时毫秒:" + intervalInMs);
System.out.println("每秒处理条数:" + avg);
}
}
- QL
import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Description:
*
* @author mwt
* @version 1.0
* @date 2020/5/21
*/
public class QLExpressTest {
@Test
public void test1() throws Exception {
String ruleExpress = "city.equals(\"杭州\") && age<=20";
long count1 = 1000 * 10000;
System.out.println("\nQLExpress1 数据量 1000 万:");
execute(count1, ruleExpress);
long count2 = 2000 * 10000;
System.out.println("\nQLExpress1 数据量 2000 万:");
execute(count2, ruleExpress);
long count3 = 4000 * 10000;
System.out.println("\nQLExpress1 数据量 4000 万:");
execute(count3, ruleExpress);
}
@Test
public void test2() throws Exception {
String ruleExpress = "city.equals(\"杭州\") && age<=20 && stringList.contains(str)";
long count1 = 1000 * 10000;
System.out.println(