MyBatis-Plus(MP)AST 抽象语法树 设计原理

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 树:

  1. 左子树:计算 name != null,返回 true 或 false。

  2. 右子树:计算 name != '',返回 true 或 false。

  3. 根节点:对两个结果做 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")

运行时执行流程(深度优先遍历):

  1. 遍历 WhereSqlNode → 判断是否需要加 WHERE 关键字。

  2. 遍历第一个 IfSqlNode → 用 OGNL 评估 name != null,为 true 则输出 "AND name = ?"。

  3. 遍历第二个 IfSqlNode → 用 OGNL 评估 age != null,为 false 则跳过。

  4. 回到 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

执行过程(后序遍历):

  1. 访问 ASTNotEq:

    • 左子树 ASTProperty:获取 name 的值 "Jack"。

    • 右子树 ASTConst:常量 null。

    • 计算 "Jack" != null → true。

  2. 访问 ASTGreater:

    • 左子树 ASTProperty:获取 age 的值 25。

    • 右子树 ASTConst:常量 18。

    • 计算 25 > 18 → true。

  3. 访问 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 执行器中是通过遍历顺序实现的。

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"的效果。

相关推荐
小师兄吃牛肉9 分钟前
Java语法 | 多重循环
java·开发语言·python
Han.miracle22 分钟前
MySQL InnoDB:MVCC、记录锁、间隙锁、二级索引MVCC底层原理
java·开发语言
不停喝水1 小时前
【前端转全栈java速通课】 项目实战④7-11节 操作数据库-登录-注册-修改密码-注销-mubatis-plus简化crud-
java·前端·数据库
y1su1 小时前
Leetcode 二分模板
java·数据结构·算法·leetcode·排序算法
霸道流氓气质1 小时前
多Agent通信机制与协议设计完全指南:从FIPA-ACL到A2A/MCP的Java生产级实战
java·开发语言
007张三丰1 小时前
C/C++ 内存管理详解:从内存分布到 new/delete 底层原理
java·c语言·c++·内存管理
骇客野人2 小时前
Java BIO / NIO / AIO 完整详解 + 编程技巧
java·开发语言·nio
霸道流氓气质2 小时前
OpenTelemetry 入门与实战:Java Agent、Spring Boot Starter与LLM调用追踪示例
java·开发语言·spring boot
m0_587383002 小时前
24 小时自助健身房系统软件开发实战指南与案例解析
java·spring boot·spring·系统架构·需求分析