表达式引擎简介
你,对,就是你,正在看这篇文章的人,我虽然不认识你,但是 我可以负责任的说,
如果你看到这个标题就心里在悄悄的呼喊:
“靠,他们连这个都有!我省事了,哇哈哈哈和”。
那么,你绝对属于百分之一的特例。 就是说,绝大多数人的绝大多数项目,是不需要
一个嵌入式的表达式引擎的。因此,提供这个功能的目的就是:
满足一小撮人的一小撮要求
但是,"一小撮人"的"一小撮要求"有很多,作为一个小众类库,为什么单单打算支持这个特性呢?
下面是我的理由:
- 这个功能是其它模块功能的基础,我们需要它
- 可能因此吸引其他的开发者对 Nutz 的兴趣
- 需要嵌入式表达式引擎的人是 Java 开发者的少数人,但是这些人也应该比 Nutz 的用户要多
- 这些人基本上编程水平要强一些
- 其他的提交者对增加这个特性没有特别强烈的反对
提醒一句, Nutz.EL不依赖MVC环境, 它在JavaSE或Android下也工作良好.
近一步介绍表达式引擎
那么它怎么使用呢?
是的,我想这可能会是你脑海里闪出的第一个问题。并且,我想你真正想问的是:“它好用吗?”
如果你脑海里第一个问题不是这个,而是:“表达式引擎是神马东东?” 那么建议你不用阅读本文了,
反正你也用不着。等你需要的时候,再读也不迟,反正这篇文章又不长。
而关于 "好用",还有下面这三层含义:
它容易使用吗?
System.out.println(El.eval("3+4*5").equals(23)); // 将打印 true,够简单吧
表达式接受的是字符串输入,输出则是一个Object对象,而Object对象本身是根据计算结果进行进行了自动封装的。
它功能强大吗?
虽然在 #一些表达式的例子 这一节我有更详细的例子,但是这里我必须要先概括几点:
它支持变量,比如
Context context = Lang.context();
context.set("a", 10);
System.out.println(El.eval(context, "a*10")); // 将打印 100
通过 Context 接口,你可以为你的表达式随意设置变量的值。它支持如下类型的 Java 数据
- 整型 - int 或 Integer
- 浮点 - float 或 Float
- 长整 - long 或 Long
- 布尔 - boolean 或 Boolean
- 字符串 - String
- 数组 -
T[]
- 列表 -
Lst<Ti>
- 集合 -
Collection<T>
- Map -
Map<String,?>
- 普通 Java 对象
基本上,有了这些,你可以为所欲为了吧。
它速度怎么样?
我觉得它速度不怎么样。它的工作的原理是这样的,每次解析都经过如果下三步
- 解析成后缀表达式形式的一个队列
- 将后缀表达式解析成一棵运算树.
- 对运算树的根结点进行运算.
当然我也提供了一个提升效率的手段,因为如果每次计算都经过这三个步骤当然慢,
所以我们可以对它先预编译:
El exp = new El("a*10"); // 预编译结果为一个 El 对象
Context context = Lang.context();
context.set("a", 10);
System.out.println(exp.eval(context)); // 将打印 100
El在实例化时就会对表达式进行预编译,会直接编译成运算树,当调用eval方法时,
就不用再耗时的编译动作了.
它的 eval 函数是线程安全的,只要在多个线程内给它不同的 context 就是了。
当然,你也可以在多个线程间共享同一个 Context,那运行起来一定很有趣,不是吗?
支持什么样的操作符
我想但凡有机会和兴趣读到这篇文字的同学,一定是编程老手,即使是自称小白的的同学们,
你们对一个编程语言应该支持的操作符基本都差不多熟的不行,
所以,我就不在这里唠叨操作符的具体细节了,我只给一个列表,告诉你我现在支持什么操作符。
另外,再加上一句:
只要支持的操作符,我会让它的优先级以及行为会和 Java 的表达式一致。如果你发现不一致 别犹豫,给我报 Issue 吧。
符号 | 操作数 | 权重 | 解释 | |
() | * | 100 | 括号,优先计算 | |
, | * | 0 | 逗号,主要是方法参数 | |
. | 2 | 1 | 访问对象的属性,或者Map的值,或者方法调用,或者自定义函数调用(需要结合后面是否有括号) | |
['abc'] | 2 | 1 | Java 对象 Map按键值获得值 | |
[3] | 2 | 1 | 数字,列表,或者集合的下标访问符号 | |
* | 2 | 3 | 乘 | |
/ | 2 | 3 | 整除 | |
% | 2 | 3 | 取模 | |
+ | 2 | 4 | 加 | |
- | 2 | 4 | 减 | |
- | 2 | 2 | 负 | |
>= | 2 | 6 | 大于等于 | |
<= | 2 | 5 | 小于等于 | |
== | 2 | 7 | 等于 | |
!= | 2 | 6 | 不等于 | |
! | 2 | 7 | 非 | |
> | 2 | 6 | 大于 | |
< | 2 | 6 | 小于 | |
&& | 2 | 11 | 逻辑与 | |
|
| 2 | 12 | 逻辑或 |
?: | 2 | 13 | 三元运算 | |
& | 2 | 8 | 位运算,与 | |
~ | 2 | 2 | 位运算,非 | |
| | 2 | 10 | 位运算,或 | |
^ | 2 | 9 | 位运算,异或 | |
<< | 2 | 5 | 位运算,左移 | |
>> | 2 | 5 | 位运算,右移 | |
>>> | 2 | 5 | 位运算,无符号右移 | |
& | 2 | 8 | 位运算,与 | |
!! | 1 | 0 | 忽略空值/异常 | |
| |
| 2 | 12 | 若A为null或者长度是0,返回B |
当然,同任何编程语言一样,表达式也支持 左括号 `(` 以及 右括号`)`,
来控制表达式的的计算优先级别
空值或异常的处理
例如obj.pet.name中的pet是null,在1.r.63开始, 可以捕捉异常然后表达式的值为null
Context ctx = Lang.context();
ctx.set("obj", new NutMap("pet", null));
assertTrue((Boolean)El.eval(ctx, "!!(obj.pet.name) == null"));
提醒一下, 双叹号后面的括号是必须的.
A或者B
有空值/异常处理后,有时候还需要填充一个默认值
Context ctx = Lang.context();
ctx.set("obj", new NutMap("pet", null));
assertEquals("wendal", El.eval(ctx, "!!(obj.pet.name) ||| 'wendal'"));
自定义Function
好吧, 你肯定要说上面的功能简直弱爆了, 就一点简单的加加减减有什么好稀奇的,
再强点的需求就没办法满足了.
确实是这样, 所以, 我们在 EL 里面添加了自定义函数,
嘿嘿, 这回强了吧. 言归正传,下面详细的说说它的使用, 以及怎么自定义.
现有的内置函数:
名称 | 参数 | 解释 | 例子 |
max | 任意个Number型 | 取出参数中最大值 | max(1, 2, 3, 4)=>4 |
min | 任意个Number型 | 取出参数中最小值 | min(1, 2, 3, 4)=>1 |
trim | 一个String | 去掉字符串两边的空格 | trim(" 1 ")=> "1" |
uuid | 一个Number(可选) | 生成UUID字符串 | uuid() uuid(16) |
base64 | 一个或两个String | 编码为base64或解码之 | base64('abc')} base64('decode', 'sfasdfsadfsa') |
urlencode | 一个或两个String | 执行URLEncode | urlencode("中文") urlencode("中文", "UTF-8") |
now | 0个或一个String | 时间格式化 | 无参数时返回当前时间的long值,否则作为SimpleDateFormat格式化当前时间 |
它们注册在 "org/nutz/conf/NutzDefaultConfig.js", 均实现了RunMethod接口.
有两种办法来添加自定义 Function: 代码注册或配置文件
通过代码的方式注册,通常是在MainSetup类(@SetupBy)的init方法内:
// register任意一个实现了RunMethod接口的对象
CustomMake.me().register("today", new TodayRunMethod());
CustomMake.me().register("ig", ioc.get(RedisIdGenerator.class));
配置文件, 建一个js/json文件,保存为 "org/nutz/conf/NutzDefaultConfig.js", 这样就能覆盖默认实现了.
{
"EL": {
"custom":[
"net.wendal.nutzbook.el.ABC", // 需要实现RunMethod和Plugin接口
"net.wendal.nutzbook.el.DEF",
... 其他Function类名
... 务必添加默认的Function哦
]
}
}
还有些什么功能?
其实我们的 EL 很强悍的, 有好多功能, 好多使用技巧等待着你的发现. 现在简单的罗列一下, EL中的一些特性吧.
忠于 Java
Nutz中的 EL 完全忠实于 JAVA 基本运算规则, 并没有做一些扩展, 比如最常见的, 数据类型转换,
在 JAVA 中进行数值运算的过程中, 是根据运算符两边的类型而最终决定运算结果的类型的,
比如:
7/3 // 将返回int型
而
(1.0 * 7)/3 // 返回double
(1.0f * 7)/3 // 则返回float
为什么后面两个返回类型不一样呢? 因为在 JAVA 中默认浮点类型是 double 哦.
基于这个原因, 在 EL 中同样保留了这些特点. 所以, 亲, 要是没返回 double 别骂我们哦~~~
支持对象方法调用
亲, EL 支持对象, 支持对象方法调用哦~~~
这有什么好大不了的? 给你看个例子, 嘿嘿:
Context context = Lang.context();
context.set("a", new BigDecimal("7"));
context.set("b", new BigDecimal("3"));
assertEquals(10, El.eval(context, "a.add(b).intValue()"));
- 看到什么没? 对了, BigDecimal 你完全可以丢各种各样的对象到 context 里面去, 然后在 EL 中调用它们的方法.
- 然后你就进行各种各样的虐待, 皮鞭, 蜡烛, 想来啥来啥...额...太邪恶了...
- 同样, EL 是使用反射的, 所以会存在一些底层异常的问题.
支持静态方法调用
没看错, EL支持静态方法调用, 不过, 在调用之前你需要设置一个变量才行...
Context context = Lang.context();
context.set("strings", Strings.class);
assertEquals("nutz", El.eval(context, "strings.trim(" nutz ")"));
一些表达式的例子
普通运算
System.out.println(El.eval("3+2*5"));
// 输出为 13
字符串操作
System.out.println(El.eval("trim(" abc ")"));
// 输出为 abc
Java 对象属性访问调用
Context context = Lang.context();
Pet pet = new Pet();
pet.setName("GFW");
context.set("pet",pet);
System.out.println(El.eval(context,"pet.name"));
// 输出为 GFW
函数调用
Context context = Lang.context();
Pet pet = new Pet();
context.set("pet",pet);
El.eval(context, "pet.setName('XiaoBai')");
System.out.println(El.eval(context,"pet.getName()"));
// 输出为 XiaoBai
数组访问
Context context = Lang.context();
context.set("x",Lang.array("A", "B", "C"));
System.out.println(El.eval(context,"x[0].toLowerCase()"));
// 输出为 a
列表访问
Context context = Lang.context();
context.set("x",Lang.list("A", "B", "C"));
System.out.println(El.eval(context, "x.get(0).toLowerCase()"));
// 输出为 a
Map 访问
Context context = Lang.context();
context.set("map",Lang.map("{x:10, y:5}"));
System.out.println(El.eval(context,"map['x'] * map['y']"));
// 输出为 50
判断
Context context = Lang.context();
context.set("a",5);
System.out.println(El.eval(context,"a>10"));
// 输出为 false
context.set("a",20);
System.out.println(El.eval(context,"a>10"));
// 输出为 true