虽然mvel吸收了大量的Java语法,但作为一个表达式语言,还是有着很多重要的不同之处,以达到更高的效率,比如:mvel像正则表达式一样,有直接支持集合、数组和字符串匹配的操作符。
除了表达式语言外,mvel还提供了用来配置和构造字符串的模板语言。
mvel2.x表达式包含以下部分的内容:属性表达式,布尔表达式,方法调用,变量赋值,函数定义。
一、基本语法
MVEL 是一种基于java语法,但又有着显著不同的表达式语言。与java不同,MVEL是动态类型(带有可选分类),也就是说在源文件中是没有类型限制的。一条MVRL表达式,简单的可以是单个标识符,复杂的则可能是一个充满了方法调用和内部集合创建的庞大的布尔表达式。
1、简单的属性表单式:user.name
在这个表达式中,我们只是有一个标识符(user.name),这就是我们所说的MVEL的AA级属性表达式,该表达式的唯一目的是获取一个变量或上下文对象的属性。属性表达式是MVEL的最常见的用途之一,通过它,MVEL可以用来作为一个高性能,易使用的反射优化器。
2、布尔表达式:
MVEl也可以用来表示一个布尔表达式,如:user.name == 'John Doe'.像java一样,MVEL支持所有优先级规则,包括通过括号来控制执行顺序,如:(user.name == 'John Doe') && ((x * 2) - 1) > 20
3、复合语句表达式:
在一段脚本里,你可以写任意多个语句,但注意要用分号来作为每个语句的结束符,只有一个语句时或最后一个语句时除外。例如:statement1; statement2; statement3 注意最后一个语句没有分号。另外,换行不能替代分号来作为一个语句的结束标识。
4、返回值:
比如,MVEL使用了输出最后值原则,也就是说,尽管MVEL定义了return关键字,但却没有必要用它。例如;
a = 10;
b = (a = a * 2) + 10;
a;
这段脚本将最后一个表达式的值a作为自己的值返回,功能上它与下面这段脚本等价:a = 10;
b = (a = a * 2) + 10;
return a;
二、操作符
下面列出了MVEL中所有的操作符:
一元操作符:
new,用来实例化对象,例:new String("foo")
with,对单个对象执行多个操作,例:with (value) { name = 'Foo', age = 18, sex = Sex.FEMALE }
assert,用一个AssertionError 断言一个值的对错,例:assert foo != null
isdef,用来判断一个变量在某个范围内是否定义,例:isdef variableName
!,布尔取反操作符,例: !true == false
比较运算符;
常见的比较运算符==,!= ,>,<,>=,<=等不再赘述
contains,判断左边的值是否包含右边的值,如: var contains "Foo"
is/instance of ,判断左边的值是否是右边的类的实例,如:var instanceof Integer
strsim,比较两个字符串的相似度,返回一个百分数,如; "foobie" strsim "foobar"
soundslike,比较两个字符串的发音,如:"foobar" soundslike "fubar"
逻辑运算符:
&&,||略
or,用于多个值间进行逻辑或运算,如:foo or bar or barfoo or 'N/A'
~=,正则表达式匹配符,如:foo ~= '[a-z].+'
按位运算符:&,|,^等
数学运算符:+,-,*,/等
其它运算符:
+,字符串连接运算,如:"foo" +"bar"
#,字符连接运算,如:1 # 2返回"12"
in,投影整个项目集合,如:(foo in list)
=,赋值运算符,如:var = "foobar"
三、值判断
在MVEL中所有的判断是否相等 都是对值的判断,而没有对引用的判断,因此表达式foo == 'bar' 等价于java中的foo.equals("bar").
1、判断值是否为emptiness(需要解释emptiness)
MVEL提供了一个特殊的字符来表示值为emptiness的情况,叫作empty,如:foo == empty,若foo满足emptiness的任何条件,这个表达式值都为true
2、为null测试
MVEL中,null和nil都可以用来表示一个空值,如:foo == null ; foo == nil;
3、强制转换
当两个不同类型且没有可比性的值进行比较时,需要将左边的值强制转换成右边的值的类型时,MVEL会应用类型强制转换系统,反之亦然。如:"123" == 123;这个表达式的值为true,因为为了执行比较,强制类型转换系统会隐式的将数字123转换成字符串。
四、列表、map和数组
在MVEL中你可以使用非常简单的语法来描述列表、map、数组,且看下面的例子:
['Bob' : new Person('Bob'), 'Michael' : new Person('Michael')]
这个表达式在等价于以下语句:
Map map = new HashMap();
map.put("Bob", new Person("Bob"));
map.put("Michael", new Person("Michael"));
用这种结构描述MVEL内部数据结构,功能非常强大,你可以在任何地方使用它,甚至可以作为参数使用,如:
something.someMethod(['foo' : 'bar']);
1、列表
列表用下面的格式来描述:[item1, item2, ...],如:["Jim", "Bob", "Smith"]
2、map
map的描述格式:[key1 : value1, key2: value2, ...],如:["Foo" : "Bar", "Bar" : "Foo"]
3、数组,格式:{item1, item2, ...},如:{"Jim", "Bob", "Smith"}
4、数组的强制转换
关于数组,需要知道的一个非常重要的方面是,它可以被强制转换成其它类型的数组,当你声明一个数组时,是不直接指定其类型的,但你可以通过将其传递给一个接收int[]类型参数的方法来指定。如:
foo.someMethod({1,2,3,4});在这种情况下,当MVEL发现目标方法接收的是一个int[],会自动的将{1,2,3,4}转换成int[]类型。
五、属性访问
对于bean属性的访问,在 Groovy, OGNL, EL等脚本语言的bean 属性表达式中已经形成了一个相对比较稳定的方式,MVEL也采用了这一方式。和其它语言必须通过底层的方法来控制权限不同的是,MVEL提供了一套独立的,统一的语法来访问属性,静态字段还有map。
1、bean properties
大多数java开发者都熟悉 getter/setter 模式,并在java对象中用它来封装属性的访问权限。例如,你可能会通过下面的方式访问一个对象的属性:
user.getManager().getName();
简便起见,在MVEL中你也可以用下面的表达式来访问:
user.manager.name
注意:当一个对象中的字段的作用域是public时,MVEL仍然倾向于通过get方法来访问其属性。
2、Null-Safe Bean Navigation
有时,当你的表达式中会含有null元素时,这时就需要你进行一个为空判断,否则就会发生错误。当你使用null-safe操作符时你可以简化这个操作:user.?manager.name
它相当于:if (user.manager != null) { return user.manager.name; } else { return null; }
3、集合
集合的遍历也可以通过简单的语法来实现:
List:可以像访问数组一样访问List,如:user[5],这等价与java代码中的user.get(5);
Map:Map的访问和访问数组也非常相似,不同的是,在访问Map时索引值可以是任意对象,如:user["foobar"]
这等价于java代码中的user.get("foobar");当Map的key是String类型时,还可以使用特殊的方式来访问,如:user.foobar,也就是允许你把map本身看成一个虚拟的对象,来访问其属性
4、字符串作数组
为了能使用属性的索引(迭代也是如此),所有的字符串都可以看成是一个数组,在MVEL中你可以用下面的方式来获取一个字符串变量的第一个字符:
foo = "My String";
foo[0]; // returns 'M'
六、常量
在脚本语言中,一段文字用来代表一个固定的值
1、字符串常量:
字符串常量可以用一对单引号或一对双引号来界定。如:
"This is a string literal"
'This is also string literal'
字符串中的特殊字符:
- // - 代表一个反斜杠.
- /n - 换行符
- /r -回车符
- /u#### - Unicode 字符(如: /uAE00)
- /### - Octal字符(如: /73)
2、数字常量
整数可以表示为十进制(基数为10),8进制(基数为8),或十六进制(基数为16)。
一个十进制数字,不从零开始(相对于8进制、16进制而言),可以表示任意数,如:125
一个八进制数,以0为前缀,后面跟着0到7内的数字
一个十六进制,以0X为前缀,后面可以跟着0-9,A-F范围内的数字
3、浮点型常量
如:10.503 // a double
94.92d // a double
14.5f // a float
4、BigInteger 和 BigDecimal型常量
如:104.39484B // BigDecimal
8.4I // BigInteger
5、布尔型常量
布尔型常量用保留关键字true和false来表示。
6、空常量
用null或nil来表示
七、程序控制
事实上,MVEL的强大已经超出了简单的表达式。它提供了一系列的程序控制操作符来提高你的脚本操作,
1、If-Then-Else
MVEL提供了完整的C/Java式的if-then-else块,如:
if (var > 0) {
System.out.println("Greater than zero!");
}
else if (var == -1) {
System.out.println("Minus one!");
}
else {
System.out.println("Something else!");
}
2、三元声明
其实就是Java中的条件表达式,如:var > 0 ? "Yes" : "No";
可以嵌套,如:var > 0 ? "Yes" : (var == -1 ? "Minus One!" : "No")
3、foreach
MVEL的强大特性之一就是其Foreach操作符,在功能和语法上,他都类似于java1.5中的for each操作符,它接收用冒号隔开的两个参数,
第一个是当前元素的一个域变量,而第二个是要迭代的集合或数组。如下所示:
count = 0;
foreach (name : people) {
count++;
System.out.println("Person #" + count + ":" + name);
}
System.out.println("Total people: " + count);
因为MVEL将字符串视作一个可以迭代的对象,所以你可以用foreach语句来迭代一个字符串(一个字符接一个字符的):
str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
foreach (el : str) {
System.out.print("[" + el + "]");
}
上面的例子会输出: [A][B][C][D][E][F][G][H][I][J][K][L][M][N][O][P][Q][R][S][T][U][V][W][X][Y][Z]
你也可以利用MVEL进行计数(从1开始):
foreach (x : 9) {
System.out.print(x);
}
输出:123456789
注意:像java5.0一样,在MVEL2.0中,可以将foreach简化成关键字for来使用,如:
for (item : collection) { ... }
4、for循环
MVEL实现了标准的C语言的for循环:
for (int i =0; i < 100; i++) {
System.out.println(i);
}
5、Do While,Do Until
和java中的意义一样,MVEL也实现了Do While,Do Until,While和Until意义正好相反。
do {
x = something();
}
while (x != null);
在语义上相当于:
do {
x = something();
}
until (x == null);
7、While, Until
MVEL中实现了标准的While,并添加了一个与之相反的Until
while (isTrue()) {
doSomething();
}
或者是:
until (isFalse()) {
doSomething();
}
八、投影和交集
简而言之,投影(也叫列表解析)是描述集合的方法之一,通过非常简单的语法,你可以检索集合中非常复杂的对象模型。假设有一个User对象的
集合,每一个User都有一个Parent。现在你想获得集合users中的所有parent的name的列表(假设Parent中有字段name),你可以这样来写:
parentNames = (parent.name in users);
你甚至可以执行嵌入式操作,设想,User对象有个集合成员叫做familyMembers,现在我们想获得一个所有家庭成员姓名的集合:
familyMembers = (name in (familyMembers in users));
你可以通过用if运算符构造的条件来过滤投影:(doSomeMethod() in listOfThings if $.shouldBeRun())
其中的$是一个占位符,用来表示被过滤的元素。其实,它就是一个存在于投影上下文内部的普通变量,你还可以用它返回一个列表的投影中的当前
元素:
($ in fooList if $.name contains 'foobie')
其它例子:
(toUpperCase() in ["foo", "bar"]); // returns ["FOO", "BAR"]
(($ < 10) in [2,4,8,16,32]); // returns [true, true, true, false, false]
($ in [2,4,8,16,32] if $ < 10); // returns [2,4,8]
下面是一个通过投影实现的快速排序算法:
/**
* Sample MVEL 2.0 Script
* "Functional QuickSort"
* by: Christopher Michael Brock, Inspired by: Dhanji Prasanna
*/
import java.util.*;
// the main quicksort algorithm
def quicksort(list) {
if (list.size() <= 1) {
list;
}
else {
pivot = list[0];
concat(quicksort(($ in list if $ < pivot)), pivot, quicksort(($ in list if $ > pivot)));
}
}
// define method to concatenate lists.
def concat(list1, pivot, list2) {
concatList = new ArrayList(list1);
concatList.add(pivot);
concatList.addAll(list2);
concatList;
}
// create a list to sort
list = [5,2,4,1,18,10,15,1,0];
// sort it!
quicksort(list);
九、赋值
MVEL允许你对表达式中的变量进行赋值,以便在运行时获取,或在表达式内部使用。因为MVEL是动态类型语言,所以你不必为了声明一个变量
而指定其类型。当然,你也可以选择指定。
str = "My String"; // valid
String str = "My String"; // valid
与java语言不同的是,当给一个指定类型的变量赋值时,MVEL会提供自动的类型转换(可行的话),如:
String num = 1;
assert num instanceof String && num == "1";
对于动态类型变量而言,你要想对其进行类型转换,你只需要将值转换成相应的类型既可:
num = (String) 1;
assert num instanceof String && num == "1";
十、
MVEL可以使用def或function关键字来定义本地函数。
函数必须是先声明后引用,唯一例外的是递归调用的时候。
1、简单示例
定义函数:
def hello() { System.out.println("Hello!"); }
定义了一个没有参数的函数hello.当调用该函数时会在控制台打印"Hello!". An MVEL-defined function works just like any regular method call,
and resolution preference is to MVEL functions over base context methods.
hello(); // calls function
2、传参和返回值
函数可以接收参数和返回一个值,看下面的例子:
def addTwo(a, b) {
a + b;
}
这个函数会接收两个参数(a和b),然后将这两个变量相加。因为MVEL遵循last-value-out原则,所以
结果将会被返回。因此,你可以这样来使用这个函数:
val = addTwo(5, 2);
assert val == 10;
当然,也可以使用return 关键字来强迫从程序内部返回一个函数值。
3、closures
MVEL支持closure,虽然,其功能与本地java函数没有任何关联。
// define a function that accepts a parameter
def someFunction(f_ptr) { f_ptr(); }
// define a var
var a = 10;
// pass the function a closure
someFunction(def { a * 10 });
十一、Lambda表达式
MVEL允许定义Lambda方法,如下所示:
threshold = def (x) { x >= 10 ? x : 0 };
result = cost + threshold(lowerBound);
上面的例子定义了一个Lambda,并将其赋值给变量"threshold".Lambda实质上就是一个用来给变量赋值的函数,也是closure
十二、宏
MVEL支持通过宏定义来用外部的可扩展资源来代替一个标记,这是它的一项基本的功能。这一功能被用来创建封装了解释器的特殊关键字。
一个宏可以由任意多个合法的标识符组成,如:modify
考虑下面的代码:
modify (obj) { value = 'foo' };
这不是一个合法的MVEL表达式,因为毕竟在MVEL中没有modify这个关键字。然而,在JBoss Drools 中会用这一功能通过用字符串
@Modify with替代modify标识符来实现结构变化监听器,
1、org.mvel.Macro 接口:
public interface Macro {
public String doMacro();
}
这个接口非常简单,执行时方法 doMacro() 会返回一个字符串来代替原有的标记,例如:
Macro modifyMacro = new Macro() {
public String doMacro() {
return "@Modify with";
}
}
2、使用MacroProcessor
MacroProcessor是一个轻量级的快速文本转换器,它使用定义的宏来替代所有相对应的标识符。定义的宏存储在一个Map中,需要匹配的标识符作为
键,宏的实例作为值,然后传递给MacroProcessor,如:
Map<String, Macro> myMacros = new HashMap<String, Macro>();
// Add modifyMacro to the Map
myMacros.put("modify", modifyMacro);
// Create the macro processor
MacroProcessor macroProcessor = new MacroProcessor();
// Add the macro map to the macro processor
macroProcessor.setMacros(myMacros);
// Now we pre-parse our expression
String parsedExpression = macroProcessor.parse(expression);
返回的字符串就会直接传递给MVEL的编译器。
十三、拦截器
MVEL提供了在编译后的表达式里使用拦截器的功能,这对实现监听器或是在表达式内部触发一个外部事件特别有用。声明拦截器用的是@Syntax,
有点像java语言中的注解。拦截器的声明应该放在待封装的语句之前,它可以实现之前或之后的监听器,或二者都实现。例如:
@Intercept
foreach (item : fooItems) {
total += fooItems.price;
}
在这个特殊的句子里,拦截器封装了整个的foreach块,因此,如果拦截器实现了之后的监听器,则当foreach循环结束后,拦截动作将被触发。
1、拦截器接口org.mvel.intergration.Interceptor
public interface Interceptor {
public int doBefore(ASTNode node, VariableResolverFactory factory);
public int doAfter(Object exitStackValue, ASTNode node, VariableResolverFactory factory);
}
拦截器接口提供了两个待实现的方法:doBefore和doAfter,下面我们来看一下MVEL运行时传递给这两个方法的参数的含义
2、doBefore
在执行封装的命令前会执行doBefore方法。
org.mvel.ASTNode::node
ASTNode句柄是 拦截器内部ASTNode 的一个引用,可以用来获取实际编译后的代码的信息。
org.mvel.integration.VariableResolverFactory::factory
变量分析器工厂提供表达式内当前范围内变量的访问权限。
3、doAfter
在执行完封装的指令后执行doAfter方法
java.lang.Object::exitStackValue
doAfter方法虽是在语句执行后执行,但却不是在帧结束前。因此,操作结束时留在栈中的任何数据都仍然存在,而且能被拦截器访问。例如:
@Intercept cost += value;
这是一个比较特殊的句子,cost的原值一直保存在栈中,直到整个帧执行完毕,因此,这个值在调用doAfter方法时可以通过exitStackValue访问到。
org.mvel.ASTNode::node
这是传递到doBefore方法中的同一个AST 元素,更多细节参考doBefore方法。
org.mvel.intergration.VariableResolverFactory::factory
同doBefore方法
4、编译器中使用拦截器
为了能是拦截器连到表达式中,必须在编译表达式之前提供拦截器,因此有一点需要注意,拦截器可能不用于MVEL解释器。
拦截器是储存在map里提供给编译器的,map中的键为拦截器的名称,值为拦截器实例。如:
// Create a new ParserContext
ParserContext context = new ParserContext();
Map<String, Interceptor> myInterceptors = new HashMap<String, Interceptor>();
// Create a simple interceptor.
Interceptor myInterceptor = new Interceptor() {
public int doBefore(ASTNode node, VariableResolverFactory factory) {
System.out.println("BEFORE!");
}
public int doAfter((Object value, ASTNode node, VariableResolverFactory factory) {
System.out.println("AFTER!");
}
};
// Now add the interceptor to the map.
myInterceptors.put("Foo", myInterceptor);
// Add the interceptors map to the parser context.
context.setInterceptors(myInterceptors);
// Compile the expression.
Serializable compiledExpression = MVEL.compileExpression(expression, context);
十四、数据类型
MVEL是一种有静态类型的动态类型语言。大部分MVEL使用者都比较倾向于用动态类型,因为它非常简单易用。如:
a = 10; // declare a variable 'a'
b = 15; // declare a variable 'b';
a + b;
1、动态类型与强制转换
像MVEL这种直接与java对象(静态类型)打交道的语言,最重要的一个方面就是强制类型转换。因为MVEL不能对一个java.lang.String对象和一个
java.lang.Integer对象进行数学运算,所以就必须把其中一个的类型转换成另一个的类型。
2、性能考虑
在你的应用中集成一个像MVEL这样的东西,性能考虑是必须的。对于重量级程序加载,强制类型转换超负荷等可以通过缓存和优化器(仅用于预编译
的表达式)来解决。然而,并不是所有的强制类型转换都可以忽略不管,关键要看它是在做什么。
比如,当一个String类型的变量在运行中要看成一个整形变量时,要阻止运行时将字符串转换成整型简直是不可能的,像这种情况,一定要考虑其性能。
3、方法调用
调用方法是强制转换的最重要的一个方面。从根本上讲,你可以直接调用,而无需关心参数是什么。解释器或编译器会分析方法的参数类型,然后确定
要进行哪一种强制转换,如果是重载的方法,它会选择与输入类型最接近的那个方法进行调用,以尽可能的避免强制转换。
4、数组
数组是强制类型转换中最有趣的一个方面,因为MVEL缺省使用无类型数组(也就是说任何情况下都是Object[]),只有当遇到类型冲突时,才会尝试将
整个数组转换成所需的类型,比如在方法调用传参时。
示例:
myArray = {1,2,3};
// pass to method that accepts String[]
myObject.someMethod(myArray);
在这个例子里,somMethod方法接收字符数组,这在MVEL中不会出错,相反,MVEL会将myArray转换成字符数组。
5、静态类型
静态类型与java类似,只不过默认情况下仍然会进行强制转换。
int num = 10;
这个句子声明了一个整型变量num,这时,MVEL运行时会强制转换类型。比如,声明后赋值一个不合适类型的数据,结果就会出现异常。
num = new HashMap(); // will throw an incompatible typing exception.
但如果是一个可以进行强制类型转换的值时,MVEL就会进行强制转换。
num = "100"; // will work -- parses String to an integer.
6、严格类型
严格类型是编译器的一种可选模式,在这种模式下,所有的类型都必须限定,不管是声明时还是在引用时。
启动严格模式:
当编译一个表达式时,可以通过ParserContext设置setStrictTypeEnforcement(true)将编译器设置成严格模式。
严格类型通过表达式内的具体类型声明或提前告诉转换器确定的类型来完成。例如:
ExpressionCompiler compiler = new ExpressionCompiler(expr);
ParserContext context = new ParserContext();
context.setStrictTypeEnforcement(true);
context.addInput("message", Message.class);
context.addInput("person", Person.class);
compiler.compile(context);
在这个例子中我们通知编译器表达式将接收两个外部输入:message 和 person 及它们的类型。这就使得编译器可以在编译时确定某一
个调用是否是安全的,从而防止了运行时的错误。
7、强类型
强类型是MVEL2.0新引入的概念。强类型模式要求所有的变量必须是限定的类型,从这一点上它与严格类型不同。差别在于严格模式只是在编译时
限定属性和方法调用的类型。
十五、Shell
通过交互式的Shell,你可以直接与MVEL打交道,去探究MVEL的特性。
1、运行Shell
只需运行MVEL的分布式jar包既可运行Shell:java -jar mvel2-2.0.jar
或者,你也可以在你喜欢的IDE中通过配置一个该类的运行环境来运行。
十六、FAQ
1、为什么不能使用.class的引用?
MVEL没有像java中的用来执行类型文件的.class标识符,其实它本身就没有class文件,而只需要通过其名称就可以引用这个类。比如,一个方法
接收一个Class类型作为参数,你可以这样来调用:
// MVEL
someMethod(String);
// Java-equivalent
someMethod(String.class);
事实上,MVEL将.class视作一个普通的bean属性,因此,如果使用String。class,那返回值就会是指向java.lang.Class本身的一个
java.lang.Class 的实例,因此就相当于在java中使用String.class.getClass() .
原理是这样的,MVEL使用动态类型系统,这样类型就被当作普通的变量来看待,而不是像java中限定类文件。所以,MVEL允许class类型作为
一个普通变量来引用,而不像java。
十七、为什么不能用object.class.name的格式?
这是MVEL的一个限制,可能会在将来的某个版本中标记出来,但bean属性不支持对Class的引用。并不是说不能调用Class的方法,你必须使用
限定的方法,像:
someVar.class.getName(); // Yes!
someVar.class.name; // No!
someVar.getClass().getName() // Yes!
someVar.getClass().name // No!
这一规定完全限制了java.lang.Class仅可用做某个变量的属性,并限制了MVEL处理类的引用的方式。