凛冽严凝雾气昏。空中瑞雪降纷纷。须臾四野难分别,顷刻山河不见痕。
银世界,玉乾坤。望中隐隐接昆仑。若还下到三更后,直要填平玉帝门。
—— 长沙今天雪下的很大,2021/12/26 晚
本文源码使用到 Jar 包版本约束如下:
- Spring Boot,2.4.12;
mybatis-spring-boot-starter
,1.3.2;
一、概述
动态 SQL 是 MyBatis 的强大特性之一。如果使用过 JDBC 或其它类似的框架,应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
例如,如下 SQL :
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE”
状态的 BLOG 都会返回;如果传入了 “title”
参数,那么就会对 “title”
一列进行模糊查找并返回对应的 BLOG 结果。
除了 if
, Mybatis 还支持 choose
,when
,otherwise
,trim
,where
等动态 SQL 元素,值得注意的是,这些元素中有些属性是需要通过表达式计算的,比如 if
元素的 test
属性,在 test
属性计算结果为 true
的情况下才会把元素内容拼接到 SQL 。
那这种表达式语法是什么呢?怎么书写呢?
二、OgnlCache
常用的表达式语言有 Spring Framework 的 SpEL,Ognl(Object-Graph Navigation Language),JEXL等,Mybatis 动态 SQL 基于 Ognl 的表达式语言。
目前,最新 Ognl Jar包版本为 ognl.3.3.0.jar
,主要 API 集中在 ognl.Ognl
类中。
Ognl 有如下几个核心概念:
- 表达式,表示要执行的操作,如上例
if
标签的test
属性; OgnlContext
,执行上下文,包含一个Object
类型的root
和Map
的非根对象集合。注意,根对象只能有一个,而非根对象可以有多个, 非根对象要通过"#key"
访问,根对象可以省略"#key"
直接访问其属性;
Mybatis 没有直接使用 ognl.Ognl
,而是对 ognl.Ognl
进行了封装 —— OgnlCache
,
public final class OgnlCache {
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
private OgnlCache() {
// Prevent Instantiation of Static Class
}
public static Object getValue(String expression, Object root) {
try {
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
}
顾名思义,OgnlCache
是在 ognl.Ognl
上增加了缓存机制,提高 Ognl 表达式计算效率,缓存主要体现在如下两个方面,
- Ognl 表达式需要解析,为了避免重复解析,Mybatis 缓存 Ognl 表达式解析结果缓存。
- Mybatis 使用自定义类解析器
OgnlClassResolver
,该类解析器会将已经解析的Class<?>
缓存;
OgnlCache 对外值提供 getValue(String expression, Object root)
,该方法在跟对象为 root
执行上下文中计算 expression
的值。
二、Ognl 语法
那我们如何在动态 SQL 中书写 Ognl 表达式,Ognl 表达式语言语法又是怎么样呢?
1. 常量
Ognl 包含如下几种形式常量:
- 字符串常量,单引号
''
或者双引号""
引起的若干个字符; - 字符常量,单引号
''
引起的若单个字符; - Ognl 除了支持 Java 中
int
(默认整数类型),long
(以l
作为后缀),float
(以f
作为后缀),double
(默认小数类型)外,还支持BigDecimal
(以b
或B
作为后缀)和BigInteger
(以h
或H
作为后缀); Boolean
常量,true
和false
;null
;
public static void main(String[] args) throws Exception {
printValueAndClass(OgnlCache.getValue("'foo'", new Object())); // foo, java.lang.String
printValueAndClass(OgnlCache.getValue("\"foo\"", new Object())); // foo, java.lang.String
printValueAndClass(OgnlCache.getValue("'b'", new Object())); // b, java.lang.Character
printValueAndClass(OgnlCache.getValue("7", new Object())); // 7, java.lang.Integer
printValueAndClass(OgnlCache.getValue("7l", new Object())); // 7, java.lang.Long
printValueAndClass(OgnlCache.getValue("7f", new Object())); // 7.0, java.lang.Float
printValueAndClass(OgnlCache.getValue("7.0", new Object())); // 7.0, java.lang.Double
printValueAndClass(OgnlCache.getValue("7b", new Object())); // 7, java.math.BigDecimal
printValueAndClass(OgnlCache.getValue("7h", new Object())); // 7, java.math.BigInteger
printValueAndClass(OgnlCache.getValue("true", new Object())); // true, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("null", new Object())); // null
}
private static void printValueAndClass(Object object) {
System.err.println(object);
if(object != null) {
System.err.println(object.getClass());
}
System.err.println();
}
2. 属性引用
Ognl 对于不同类型对象处理属性引用方式不同,
Map
,以key
作为属性名引用属性值value
;List
或者数组,对于数字类型属性,作为下标索引List
或者数组中的元素,对于字符串类型属性,处理方式和简单对象处理属性引用方式相同;- 简单对象,只能处理字符串类型属性,如果该属性存在
get/set
方法,则使用该属性get/set
方法访问该属性,否则访问和给定属性名相同的字段;
public static void main(String[] args) throws Exception {
Map<Object, Object> map = Maps.newHashMap();
map.put("foo", "bar");
map.put("Aircraft", Lists.newArrayList("Airplane", "Helicopter"));
map.put("Color", new Object[] { "Red", "Green" });
// Map 直接访问 key
printValueAndClass(OgnlCache.getValue("foo", map)); // bar, java.lang.String
// List 属性为数字, 作为下标索引
printValueAndClass(OgnlCache.getValue("Aircraft[0]", map)); // Airplane, java.lang.String
// List 属性为字符串, 访问其相应字段
printValueAndClass(OgnlCache.getValue("Aircraft.size", map)); // 2, class java.lang.Integer
// 数组属性为数字, 作为下标索引
printValueAndClass(OgnlCache.getValue("Color[0]", map)); // Red, java.lang.String
// 数组属性为字符串, 访问其相应字段
printValueAndClass(OgnlCache.getValue("Color.length", map)); // 2, java.lang.Integer
// 访问简单对象属性
printValueAndClass(OgnlCache.getValue("name", new Person("foo"))); // foo, java.lang.String
}
3. 检索
Ognl 内部处理 array.length
表达式与表达式 array["length"]
完全相同,并且表达式 array["len" + "gth"]
和前面两个表达式执行结果相同。
publicstatic void main(String[] args) throws Exception {
Map<Object, Object> map = Maps.newHashMap();
map.put("array", new Object[] {});
printValueAndClass(OgnlCache.getValue("array.length", map)); // 0, java.lang.Integer
printValueAndClass(OgnlCache.getValue("array[\"length\"]", map)); // 0, java.lang.Integer
printValueAndClass(OgnlCache.getValue("array[\"len\" + \"gth\"]", map)); // 0, java.lang.Integer
}
对于 Java 数组和 List
索引比较简单,只需要给出下标即可检索出相应元素,下标超出数组或 List
的边界时,抛 IndexOutOfBoundsException
异常。
public static void main(String[] args) throws Exception {
Map<Object, Object> map = Maps.newHashMap();
map.put("array", new Object[] {});
// 抛「ArrayIndexOutOfBoundsException」异常, IndexOutOfBoundsException 子类
printValueAndClass(OgnlCache.getValue("array[0]", map));
}
JavaBeans 支持索引属性的概念,具体来说,这意味着一个对象具有一组遵循以下模式的方法:
public PropertyType[] getPropertyName()
;public void setPropertyName(PropertyType[] anArray)
;public PropertyType getPropertyName(int index)
;public void setPropertyName(int index, PropertyType value)
;
Ognl 可以解释这一点,并通过索引下标提供对属性的无缝访问。
例如,定义 Aircraft
类如下:
public class Aircraft {
private String[] colors = new String[1024];
public Aircraft(String[] colors) {
super();
this.colors = colors;
}
public String[] getColor() {
return colors;
}
public void setColor(String[] colors) {
this.colors = colors;
}
public String getColor(int index) {
return colors[index];
}
public void setColor(int index, String color) {
colors[index] = color;
}
public String[] getColors() {
return colors;
}
public void setColors(String[] colors) {
this.colors = colors;
}
}
该类包含 String[] getColor()
,setColor(String[] colors)
,getColor(int index)
,setColor(int index, String color)
方法,所以该类具有索引属性 color
,可以通过索引下标访问:
publicstatic void main(String[] args) throws Exception {
// Red, java.lang.String
printValueAndClass(OgnlCache.getValue("color[0]", new Aircraft(new String[] { "Red", "Green" })));
}
Ognl 扩展了索引属性的概念可以包括使用任意对象索引,而不仅仅是像 JavaBeans 索引属性那样只可以使用整数进行索引。 在以对象进行索引时,Ognl 查找具有以下签名的方法:
public PropertyType getPropertyName(IndexType index)
;public void setPropertyName(IndexType index, PropertyType value)
;
PropertyType
和 IndexType
必须在相应的 set 和 get 方法中匹配,例如,Aircraft
如下:
public class Aircraft {
private Map<String, String> map = Maps.newHashMap();
public String getAttribute(String name) {
return map.get(name);
}
public void setAttribute(String name, String value) {
map.put(name, value);
}
}
Aircraft
提供了 public String getAttribute(String name)
和 public void setAttribute(String name, String value)
方法,可以使用对象索引:
publicstatic void main(String[] args) throws Exception {
Aircraft aircraft = new Aircraft();
// 对象索引赋值
printValueAndClass(OgnlCache.getValue("attribute[\"foo\"] = 'bar'", aircraft)); // bar, java.lang.String
// 对象索引获取值
printValueAndClass(OgnlCache.getValue("attribute[\"foo\"]", aircraft)); // bar, java.lang.String
}
从上例可以看出,对于对象索引,可以使用相同 Ognl 表达式赋值和获取值。
4. 方法调用
Ognl 调用方法的方式与 Java 略有不同,因为 Ognl 需要在除了提供的实际参数之外没有额外类型信息情况下,运行时选择正确方法。 Ognl 总是选择能与提供的实际参数匹配的最具体的方法执行,如果有多个方法能与提供的实际参数匹配且具体程度相同,那么选择其中任意一个执行。
特别地,参数「 null
」 能够匹配所有非原始类型,因此其最有可能导致意想不到的方法调用。
public class Aircraft {
public void print(String value) {
System.err.println("Argument[ String ] method invoked");
}
public void print(Object obj) {
System.err.println("Argument[ Object ] method invoked");
}
}
Aircraft
包含两个单参数方法 print()
,看下传 null
时,直接使用Aircraft
调用 和 Ognl 调用分别会调用哪个方法:
public static void main(String[] args) throws Exception {
Aircraft aircraft = new Aircraft();
aircraft.print(null); // Argument[ String ] method invoked
printValueAndClass(OgnlCache.getValue("print(null)", aircraft)); // Argument[ Object ] method invoked
}
5. 变量
Ognl 变量语法比较简单,变量可以用来存储运算中间结果以便再次使用,或者用来提高表达式可读性。在 Ognl 中,所有变量在整个表达式中都是全局的,引用变量只需在变量名称前面加井号 「 "#"
」,例如,#var
。
public static void main(String[] args) throws Exception {
printValueAndClass(OgnlCache.getValue("#var = 'name', #var.equals('name')", new Object())); // true, java.lang.Boolean
}
Ognl 还在表达式计算的每个点将当前对象存储在变量 「 this
」中,可以像引用其他变量一样引用该变量。
例如,如下 Ognl 表达式对数组 array
的长度进行操作 —— 如果长度大于 7
,则返回长度的 2
倍,否则返回长度加 1
:
public static void main(String[] args) throws Exception {
Map<Object, Object> map = Maps.newHashMap();
map.put("array", new Object[] {new Object(), new Object()});
// 3, java.lang.Integer
printValueAndClass(OgnlCache.getValue("array[\"length\"].(#this > 7 ? 2*#this : 1 + #this)", map));
}
变量显示赋值语法,和 Java 或者其他语言一样,例如 #var = 99
。
6. 括号表达式
用括号 —— "()"
括起来的表达式将和周围运算符分开,作为一个整体单元运算,这可以强制改变 Ognl 运算符默认优先级,这也是在方法参数中使用逗号 —— “,”
运算符的唯一方式。
public static voidmain(String[] args) throws Exception {
// true, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("'foo'.equals(('bar', 'foo'))", new Object()));
}
7. 链式子表达式
如果在点 —— "."
后使用括号表达式,那么 "."
前计算结果对象将被做括号表达式的当前对象。
例如,如下 Ognl 表达式在 "."
前构造了一个 Map
,该 Map
将作为 "."
后括号表达式 —— (foo)
的当前对象。
public static void main(String[] args) throws Exception {
// foo value, java.lang.String
printValueAndClass(OgnlCache.getValue("#{ 'foo' : 'foo value', 'bar' : 'bar value'}.(foo)", new Object()));
}
8. 集合构造
构造 List
,需要把集合中元素放在大括号 —— "{}"
中,和方法参数一样,集合中元素表达式不能包含逗号,除非放在括号 —— "()"
中。
例如:
public static void main(String[] args) throws Exception {
// [null, Untitled], java.util.ArrayList
printValueAndClass(OgnlCache.getValue("{ null, 'Untitled'}", new Object()));
// [null, bar], java.util.ArrayList
printValueAndClass(OgnlCache.getValue("{ null, ('foo', 'bar')}", new Object()));
}
如下 Ognl 表达式检查属性 name
是否为 null
或者 "Untitled"
,
public static void main(String[] args) throws Exception {
Map<Object, Object> map = Maps.newHashMap();
map.put("name", "Untitled");
printValueAndClass(OgnlCache.getValue("name in { null,\"Untitled\" }", map)); // true, java.lang.Boolean
}
Ognl 创建数组方式和 Java 大同小异 —— Ognl 支持类似调用构造函数方式创建,并且允许创建时初始化,或者给定数组大小。如果使用数组大小构造,则数组中所有元素都为 null
或者 0
。
public static void main(String[] args) throws Exception {
printValueAndClass(OgnlCache.getValue("new int[] { 1, 2, 3 }", new Object())); // [I@3fb6a447, class [I
printValueAndClass(OgnlCache.getValue("(new int[5])[0]", new Object())); // 0, java.lang.Integer
}
Ognl 使用一种特殊语法构造 Map
,
public static void main(String[] args) throws Exception {
// {foo=foo value, bar=bar value}, class java.util.LinkedHashMap
printValueAndClass(OgnlCache.getValue("#{ 'foo' : 'foo value', 'bar' : 'bar value' }", new Object()));
}
以上 Ognl 表达式创建了一个 Map
,并初始化了键 "foo"
和 "bar"
。
如果要指定特定 Map
,可以在左大括号 —— "{"
前指定该 Map
实现类全类名,
public static void main(String[] args) throws Exception {
// {bar=bar value, foo=foo value}, java.util.HashMap
printValueAndClass(OgnlCache.getValue("#@java.util.HashMap@{ \"foo\" : \"foo value\", \"bar\" : \"bar value\" }", new Object()));
}
9. 集合元素选择
Ognl 提供了一种简单语法使用表达式从集合中选择某些元素构成新集合,好比从数据库表所有行中查询子集。
例如,如下表达式选择集合中所有 String
类型元素:
public static void main(String[] args) throws Exception {
// [foo, bar], java.util.ArrayList
printValueAndClass(OgnlCache.getValue("{'foo', 1, 'bar'}.{? #this instanceof String}", new Object()));
}
如果要选择第一个匹配元素,可以使用索引,比如 "{'foo', 1, 'bar'}.{? #this instanceof String}[0]"
,但是如果没有匹配元素,使用索引访问就会报 ArrayIndexOutOfBoundsException
异常。
这种语法适用于选择第一个匹配元素,并作为 List
返回,只需要将 「 ?
」改成 「 ^
」,和正则表达式类似 。
如果没有匹配任何元素,则返回空 List
。
public static void main(String[] args) throws Exception {
// [foo], java.util.ArrayList
printValueAndClass(OgnlCache.getValue("{'foo', 1, 'bar'}.{^ #this instanceof String}", new Object()));
}
同理,选择匹配元素的最后一个元素,只需要将 「 ?
」改成 「 $
」:
public static void main(String[] args) throws Exception {
// [bar], java.util.ArrayList
printValueAndClass(OgnlCache.getValue("{'foo', 1, 'bar'}.{$ #this instanceof String}", new Object()));
}
10. 调用构造方法
在 Ognl 表达式中可以像在 Java 一样使用 「 new
」运算符创建对象,不一样的是,除了 java.lang
包中的类以外,其他类需要使用全限定类名。
public static void main(String[] args) throws Exception {
// java.lang.Object@380fb434, java.lang.Object
printValueAndClass(OgnlCache.getValue("new Object()", new Object()));
// Wed Dec 29 19:21:18 CST 2021, java.util.Date
printValueAndClass(OgnlCache.getValue("new java.util.Date()", new Object()));
}
11. 调用静态方法
在 Ognl 表达式中可以使用这种语法调用静态方法 「 @class@method(args)
」,如果不写「 class
」,默认「 java.lang.Math
」,以便更方便地使用 min
和 max
方法, 同样「 class
」需要为全类名。
public static void main(String[] args) throws Exception {
// 1640766639837, java.lang.Long
printValueAndClass(OgnlCache.getValue("@java.lang.System@currentTimeMillis()", new Object()));
printValueAndClass(OgnlCache.getValue("@@max(7, 9)", new Object())); // 9, java.lang.Integer
}
如果已经有静态方法所属类对象,可以就像调用实例方法一样调用该实例类的静态方法,
public class OgnlTest {
public static void print() {
System.err.println("foo");
}
public static void main(String[] args) throws Exception {
// null
printValueAndClass(OgnlCache.getValue("print()", new OgnlTest()));
}
}
12. 访问静态字段
在 Ognl 表达式中可以使用这种语法调用静态方法 「 @class@field
」,同样「 class
」需要为全类名。
13. 表达式求值
如果在 Ognl 表达式后面跟着一个括号 —— "()"
,但是括号前没有点 —— "."
,那么 Ognl 就会把括号前 Ognl 表达式计算结果作为新的 Ognl 表达式,并且以括号中 Ognl 表达式计算结果作为根对象再次计算。
例如,
public class OgnlTest {
public static String print() {
return "foo";
}
public static void main(String[] args) throws Exception {
// foo value, java.lang.String
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print()(#{ 'foo' : 'foo value', 'bar' : 'bar value' })", new OgnlTest()));
}
}
上例先使用表达式 「@test.OgnlTest@print()
」调用静态方法 print()
,得到结果 "foo"
,再使用该字符串作为新表达式,括号中 —— #{ 'foo' : 'foo value', 'bar' : 'bar value' }
创建的 Map
作为根对象再次计算。
14. 和 Java 运算符异同
Ognl 大部分运算符借鉴至 Java,所以使用方法也类似,这里罗列出一些和 Java 不同的一些运算符。
- 逗号运算符 「
,
」,这个运算符借鉴至 C,逗号运算符用于分割两个独立的表达式,并且逗号运算符计算结果为第二个表达式计算结果。例如,「ensureLoaded(), name
」, 在执行该 Ognl 表达式时,先调用方法ensureLoaded()
,然后再检索name
; - 花括号 「
{}
」构造列表,可以将列表初始值放在花括号中创建列表,例如,「{ null, true, false }
」; - 「
in
」运算符,可以还是用该运算符判断值是否在集合中,例如,「name in {null,"Untitled"} || name
」;
15. 类型转换
Ognl 可以将对象转换成其他类型,比如布尔类型,数字类型,集合类型等,看下转换规则。
在必要时,对象可以转换成布尔类型,
- 如果对象本来就是「
Boolean
」类型,直接取值即可; - 如果对象为数字类型,规则和 C 类似 —— 取其双精度浮点值和零比较,非零即为
true
,零为false
; - 如果对象为「
Character
」类型,其char
值非零时对应true
,否则为false
; - 其他对象, 非
null
为true
,null
为false
;
public class OgnlTest {
public static Boolean print(boolean b) {
return b;
}
public static void main(String[] args) throws Exception {
// false, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print(0.0)", new OgnlTest()));
// true, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print(1)", new OgnlTest()));
// false, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print('\0')", new OgnlTest()));
// true, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print('f')", new OgnlTest()));
// true, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print(new Object())", new OgnlTest()));
// false, java.lang.Boolean
printValueAndClass(OgnlCache.getValue("@test.OgnlTest@print(null)", new OgnlTest()));
}
}
对象向其他类型转换规则参考 commons-ognl language-guide。
16. 运算符
Ognl 借鉴了 Java 的大部分运算符,并添加了一些新运算符。 在大多数情况下,Ognl 对给定运算符的处理与 Java 相同。
值得注意的是,是 Ognl 本质上是一种无类型语言, 这意味着 Ognl 中的每个值都是一个 Java Object,并且 Ognl 试图在特定语境中将对象进行转换。
下表列出了 Ognl 常用运算符,优先级数字越小,优先级越低,相同优先级从左到右依次计算。
优先级 | 运算符 | Ognl.getValue() |
---|---|---|
1 | e1, e2 | 逗号运算符,e1, e2 在相同源对象中计算,并返回 e2 计算结果 |
2 | e1 = e2 | 赋值运算符 |
3 | e1 ? e2 : e3 | |
4 | e1 || e2, e1 or e2 | |
5 | e1 && e2, e1 and e2 | |
6 | e1 | e2, e1 bor e2 | |
7 | e1 ^ e2, e1 xor e2 | |
8 | e1 & e2, e1 band e2 | |
9 | e1 == e2, e1 eq e2 | |
9 | e1 != e2, e1 neq e2 | |
10 | e1 < e2, e1 lt e2 | |
10 | e1 <= e2, e1 lte e2 | |
10 | e1 > e2, e1 gt e2 | |
10 | e1 >= e2, e1 gte e2 | |
10 | e1 in e2 | |
10 | e1 not in e2 | |
11 | e1 << e2, e1 shl e2 | |
11 | e1 >> e2, e1 shr e2 | |
11 | e1 >>> e2, e1 ushr e2 | |
12 | e1 + e2 | |
12 | e1 - e2 | |
13 | e1* e2 | |
13 | e1 / e2 | |
13 | e1 % e2 | |
14 | + e | |
14 | - e | |
14 | ! e, not e | |
14 | ~ e | |
14 | e instanceof class | |
15 | e.method(args) | |
15 | e.property | |
15 | e1[ e2 ] | |
15 | e1.{ e2 } | |
15 | e1.{? e2 } | |
15 | e1.(e2) | |
15 | e1(e2) | |
16 | constant | |
16 | ( e ) | |
16 | method(args) | |
16 | property | |
16 | [ e ] | |
16 | { e, … } | |
16 | #variable | |
16 | @class@method(args) | |
16 | e1 in e2@class@field | |
16 | new class(args) | |
16 | new array-component-class[] { e, … } | |
16 | #{ e1 : e2, … } | |
16 | #@classname@{ e1 : e2, … } | |
16 | :[ e ] |