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,返回 truefalse

  2. 右子树:计算 name != '',返回 truefalse

  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 的三大核心优势

优势 说明 例子
语法树结构 清晰表达运算符优先级和嵌套关系 andor 的优先级通过树形结构自然体现,不需要手动加括号
类型安全 在遍历 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> 标签的智能行为)。

这里的 IfSqlNodeWhereSqlNode 都是 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" != nulltrue

  2. 访问 ASTGreater

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

    • 右子树 ASTConst:常量 18

    • 计算 25 > 18true

  3. 访问 ASTAnd

    • 左子树结果 true,右子树结果 true

    • 计算 true and truetrue

最终返回 true,该 <if> 标签内容被拼接。


AST 带来的"坑"与注意事项

1. 短路求值(Short-Circuit Evaluation)

OGNL 支持 andor 的短路求值:

XML 复制代码
<if test="user != null and user.name == 'Jack'">

如果 user == nulluser.name 就不会被访问,避免 NPE。这在 AST 执行器中是通过遍历顺序实现的。

OGNL 3.0+ 支持 ?. 操作符:

XML 复制代码
<if test="user?.address?.city == 'Beijing'">

如果 useraddressnull,整个表达式返回 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 ASTSqlNode 树)
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"的效果。

相关推荐
宠友信息1 小时前
消息撤回与已读状态如何在即时通讯源码中统一管理
java·spring boot·websocket·mysql·uni-app
小小放舟、1 小时前
Windows 本地安装 Elasticsearch 8.10.0 与 IK 分词器(2026)
java·大数据·windows·spring boot·elasticsearch·搜索引擎·全文检索
zzz_23681 小时前
【Java实习面试算法冲刺】总复盘
java·算法·面试
從南走到北1 小时前
JAVA无人共享系统无人场馆预约篮球系统源码支持小程序+公众号+APP+H5
java·开发语言·小程序
码智社1 小时前
Maven 零基础完整教程
java·开发语言·maven
半夜修仙1 小时前
Spring集成邮箱功能
java·开发语言·spring boot·spring·rabbitmq·github·maven
曾阿伦2 小时前
Maven 构建工程化指南
java·maven
宠友信息2 小时前
从重复请求到订单同步看内容社区源码的处理思路
java·spring boot·redis·后端·mysql·spring
带刺的坐椅2 小时前
用 Solon TeamAgent 落地供应链异常管理与自动补货
java·ai·llm·agent·solon·供应链