AST 全称是 Abstract Syntax Tree (抽象语法树),是源代码语法结构的一种抽象表示。
简单来说:编译器或解释器在解析一段代码(或表达式)时,会先将文本字符串转换成一棵树形结构,这棵树就是 AST。
直观理解:从文本到树
假设有一句简单的算术表达式:
3 + 5 * 2
人类一眼就能看出要先算 5 * 2,再算 3 + 10。但计算机不认识,它需要先解析:
文本 → 词法分析(Token 流) → 语法分析(AST) → 执行/编译
AST 长这样(简化版):
+
/ \
3 *
/ \
5 2
这棵树清晰地表达了运算优先级 :* 是 + 的子节点,所以先算乘法。
AST 在 OGNL 中的应用
当 MyBatis 执行一个 OGNL 表达式时,比如:
java
name != null and name != ''
第一步:词法分析(Lexical Analysis)
将字符串拆解成一个个 Token(最小语法单元):
bash
Token: [name] [!=] [null] [and] [name] [!=] ['']
第二步:语法分析(Syntax Analysis)
根据语法规则,构建 AST 树:
bash
and
/ \
!= !=
/ \ / \
name null name ''
第三步:执行(Evaluation)
MyBatis 会深度优先遍历这棵 AST 树:
-
左子树:计算
name != null,返回true或false。 -
右子树:计算
name != '',返回true或false。 -
根节点:对两个结果做
and运算,返回最终布尔值。
为什么要用 AST?直接字符串判断不行吗?
如果只做简单的 if (name != null),用字符串 split 或正则也能凑合。但 OGNL 需要支持极其复杂的表达式:
XML
<!-- 真实项目中的复杂判断 -->
<if test="user != null and user.age >= 18 and (user.role == 'admin' or user.role == 'super_admin') and @com.example.util.SecurityUtil@hasPermission('delete')">
DELETE FROM user WHERE id = #{id}
</if>
用 AST 的三大核心优势
| 优势 | 说明 | 例子 |
|---|---|---|
| 语法树结构 | 清晰表达运算符优先级和嵌套关系 | and 和 or 的优先级通过树形结构自然体现,不需要手动加括号 |
| 类型安全 | 在遍历 AST 时可以进行类型检查 | 能识别 name.length() 是方法调用,而不是属性访问 |
| 性能优化 | 表达式可以预编译成 AST 并缓存,避免重复解析 | MyBatis 的 OgnlCache 就是将表达式字符串 → AST 的映射缓存起来 |
AST 在 MyBatis 动态 SQL 中的另一个角色:XML 标签解析
不仅仅是 OGNL,MyBatis 解析 XML 中的动态标签(<if>、<where>、<foreach>)时,也是在内存中构建了一棵 AST 树。
示例 XML:
XML
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
MyBatis 在启动时构建的 AST(简化版):
bash
SqlNode (Root)
|
StaticTextSqlNode ("SELECT * FROM user ")
|
WhereSqlNode
/ \
IfSqlNode IfSqlNode
(name) (age)
| |
OGNL判断 OGNL判断
(name!=null) (age!=null)
| |
TextNode TextNode
("AND name") ("AND age")
运行时执行流程(深度优先遍历):
-
遍历
WhereSqlNode→ 判断是否需要加WHERE关键字。 -
遍历第一个
IfSqlNode→ 用 OGNL 评估name != null,为true则输出"AND name = ?"。 -
遍历第二个
IfSqlNode→ 用 OGNL 评估age != null,为false则跳过。 -
回到
WhereSqlNode,决定是否需要去掉多余的AND(这就是<where>标签的智能行为)。
这里的 IfSqlNode、WhereSqlNode 都是 SqlNode 接口的实现类 ,它们构成了一棵多叉树。
OGNL 的 AST 具体长什么样?(源码级别)
在 MyBatis 源码中,当调用 OgnlCache.getValue(expression, rootObject) 时:
java
// 1. 先从缓存取 AST(避免重复解析)
Object node = OgnlCache.getExpression(expression);
if (node == null) {
// 2. 如果缓存没有,调用 OGNL 解析器
node = Ognl.parseExpression(expression); // 返回 AST 根节点
OgnlCache.putExpression(expression, node);
}
// 3. 在上下文中执行 AST
return Ognl.getValue(node, context, rootObject);
Ognl.parseExpression() 返回的 AST 是一个 SimpleNode 对象树,每个节点代表一个操作符或操作数。
常见 AST 节点类型(OGNL 内部):
| AST 节点类 | 代表含义 | 示例 |
|---|---|---|
ASTAnd |
逻辑与 | a and b |
ASTOr |
逻辑或 | a or b |
ASTEq |
等于比较 | a == b |
ASTNotEq |
不等于比较 | a != b |
ASTGreater |
大于比较 | a > b |
ASTMethod |
方法调用 | user.getName() |
ASTProperty |
属性访问 | user.name |
ASTStaticMethod |
静态方法调用 | @Util@method() |
ASTSequence |
逗号表达式 | a, b, c |
ASTConst |
常量值 | 123、"hello"、null |
一个完整的执行示例
假设参数是 User 对象,其中 name = "Jack",age = 25。
OGNL 表达式: name != null and age > 18
AST 树(简化):
bash
ASTAnd
/ \
ASTNotEq ASTGreater
/ \ / \
name null age 18
执行过程(后序遍历):
-
访问
ASTNotEq:-
左子树
ASTProperty:获取name的值"Jack"。 -
右子树
ASTConst:常量null。 -
计算
"Jack" != null→true。
-
-
访问
ASTGreater:-
左子树
ASTProperty:获取age的值25。 -
右子树
ASTConst:常量18。 -
计算
25 > 18→true。
-
-
访问
ASTAnd:-
左子树结果
true,右子树结果true。 -
计算
true and true→true。
-
最终返回 true,该 <if> 标签内容被拼接。
AST 带来的"坑"与注意事项
1. 短路求值(Short-Circuit Evaluation)
OGNL 支持 and 和 or 的短路求值:
XML
<if test="user != null and user.name == 'Jack'">
如果 user == null,user.name 就不会被访问,避免 NPE。这在 AST 执行器中是通过遍历顺序实现的。
2. 空值安全的属性导航(Safe Navigation)
OGNL 3.0+ 支持 ?. 操作符:
XML
<if test="user?.address?.city == 'Beijing'">
如果 user 或 address 为 null,整个表达式返回 null 而非 NPE。在 AST 中对应 ASTChain 节点。
3. 性能考量
虽然 OGNL 表达式会被缓存为 AST,但如果 XML 中有成百上千个 <if> 标签 ,启动时解析并构建 AST 树仍会消耗一定时间(不过通常可忽略)。MP 通过 TableInfoHelper 缓存 和 OgnlCache 双层缓存做了优化。
总结:从文本到 SQL 的完整链路(AST 视角)
| 阶段 | 输入 | 处理 | 输出 | AST 角色 |
|---|---|---|---|---|
| 1. XML 解析 | <if test="name != null"> |
XML DOM 解析 | 生成 IfSqlNode 对象 |
XML AST (SqlNode 树) |
| 2. OGNL 解析 | "name != null" 字符串 |
Ognl.parseExpression() |
生成 OGNL AST(SimpleNode 树) |
OGNL AST(表达式树) |
| 3. 运行时判断 | 参数对象 User(name="Jack") |
遍历 OGNL AST 并计算 | 返回 true / false |
执行 AST(深度优先遍历) |
| 4. SQL 拼接 | 所有 SqlNode 按遍历结果拼装 |
遍历 XML AST | 带 ? 的预编译 SQL |
执行 XML AST |
| 5. 参数绑定 | name = "Jack" |
TypeHandler 设置参数 |
完整 SQL(日志中) | 与 AST 无关 |
一句话概括:
AST 是编程语言的"骨架",它将人类可读的代码/表达式转化为计算机可遍历和执行的树形结构。在 MyBatis 中,XML 动态标签和 OGNL 表达式分别被构建为两棵 AST,运行时通过遍历这两棵树,实现了"动态生成 SQL"的效果。