由 Mybatis 源码畅谈软件设计(四):动态 SQL 执行流程

news/2025/10/29 20:27:45/文章来源:https://www.cnblogs.com/Jcloud/p/19175314

本节我们探究动态 SQL 的执行流程,由于在前一节我们已经对各个组件进行了详细介绍,所以本节不再赘述相关内容,在本节中主要强调静态 SQL 和动态 SQL 执行的不同之处。在这个过程中,SqlNode 相关实现值得关注,它为动态 SQL 标签都定义了专用实现类,遵循单一职责的原则,并且应用了 装饰器模式。最后,我们还会讨论动态 SQL 避免注入的解决方案,它是在 Mybatis 中不可略过的一环。

动态 SQL 执行流程

以单测 org.apache.ibatis.session.SqlSessionTest#dynamicSqlParse 为例,动态 SQL 执行查询时,第一个需要注意点是获取 BoundSql 对象:

public final class MappedStatement {// sqlSource 存储 SQL 语句,区分静态、动态SQLprivate SqlSource sqlSource;public BoundSql getBoundSql(Object parameterObject) {BoundSql boundSql = sqlSource.getBoundSql(parameterObject);// ...}// ...
}

在讲解 MappedStatement 时,我们提到了包含动态标签和 $ 符号的 SQL 会被解析成 DynamicSqlSource,所以它在获取 BoundSql 时会执行如下逻辑:

public class DynamicSqlSource implements SqlSource {private final Configuration configuration;private final SqlNode rootSqlNode;public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {this.configuration = configuration;this.rootSqlNode = rootSqlNode;}public BoundSql getBoundSql(Object parameterObject) {// 创建动态 SQL 的上下文信息DynamicContext context = new DynamicContext(configuration, parameterObject);// 根据上下文信息拼接 SQL,处理 SQL 中的动态标签// 处理完成后 SQL 为不包含任何动态标签,但可能包含 #{} 占位符的 SQL 信息,SQL 会被封装到上下文的 sqlBuilder 对象中rootSqlNode.apply(context);// 处理拼接完成后 SQL 中的 #{} 占位符,将占位符替换为 ?SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();// 解析完成后的 SqlSource 均为 StaticSqlSource 类型,其中记录解析完成后的完整 SQLSqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// StaticSqlSource 获取 BoundSql SQL 的方法就非常简单了:将 SQL 和参数信息记录下来BoundSql boundSql = sqlSource.getBoundSql(parameterObject);// 在 BoundSql 对象中 additionalParameters Map 中添加 key 为 _parameter,value 为入参 的附加参数信息context.getBindings().forEach(boundSql::setAdditionalParameter);return boundSql;}
}

首先它会创建动态 SQL 上下文信息 DynamicContext,这里并不复杂,所以不再追溯源码信息。rootSqlNode 对象在讲解映射配置时我们提到过,它会被解析成 MixedSqlNode 类型,其中包含着各个节点的信息,如下所示:

sqlNode2.png

MixedSqlNode 会根据上下文信息完成 apply 操作,如注释信息所述,最终会将带有动态标签的多个节点的 SQL 解析成一条 SQL 字符串记录在上下文中。下面我们重点看一下 动态标签 的处理逻辑,它使用到了 装饰器模式静态代理模式WhereSqlNode 实现了 TrimSqlNode,但是它几乎并没有承载任何功能,只是定义了 SQL 连接符信息,这个实现类起到更多的作用是增强代码可读性和遵守单一职责的原则:

public class WhereSqlNode extends TrimSqlNode {private static final List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t","OR\t");public WhereSqlNode(Configuration configuration, SqlNode contents) {super(configuration, contents, "WHERE", prefixList, null, null);}}

处理逻辑均在 TrimSqlNode 中实现,它在其中定义了 SqlNode contents,其中最重要的是 apply 方法,装饰器模式便体现在这里:它对组合进来的其他 SqlNodeapply 方法进行增强,添加处理前缀和后缀标识符信息的逻辑,如下所示:

public class TrimSqlNode implements SqlNode {private final SqlNode contents;@Overridepublic boolean apply(DynamicContext context) {FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);boolean result = contents.apply(filteredDynamicContext);// 处理前缀和后缀标识符信息filteredDynamicContext.applyAll();return result;}private class FilteredDynamicContext extends DynamicContext {// ...}
}

WhereSqlNode.drawio.png

实现处理前缀和后缀表示逻辑的 FilteredDynamicContext 是定义在 TrimSqlNode 中的内部类,它使用到了静态代理模式,在 Mybatis 框架中,出现 delegate 字段命名时,便需要对代理模式多留意了,而且这种命名也提醒我们,未来在使用到代理模式时,可以将被代理对象命名为 delegate

DynamicContext delegate 对象被代理,由代理对象 FilteredDynamicContext 完成前后缀处理,最后将处理完的 SQL 拼接到原上下文中:

public class TrimSqlNode implements SqlNode {// ...private class FilteredDynamicContext extends DynamicContext {private final DynamicContext delegate;private boolean prefixApplied;private boolean suffixApplied;private StringBuilder sqlBuffer;public void applyAll() {sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);if (trimmedUppercaseSql.length() > 0) {// 处理前缀标识符比如,WHERE,SETapplyPrefix(sqlBuffer, trimmedUppercaseSql);// 处理后缀标识符,一般用于自定义 TrimSqlNodeapplySuffix(sqlBuffer, trimmedUppercaseSql);}delegate.appendSql(sqlBuffer.toString());}}}

这段逻辑并不复杂,除此之外我们需要再关注下 IfSqlNode 的逻辑,探究 IF 标签 中的内容是如何被拼接到 SQL 中的:

public class IfSqlNode implements SqlNode {private final ExpressionEvaluator evaluator;private final String test;private final SqlNode contents;@Overridepublic boolean apply(DynamicContext context) {// 判断表达式,如果 if 标签中 test 判断为 true 则将对应的 SQL 片段拼接到 SQL 上if (evaluator.evaluateBoolean(test, context.getBindings())) {contents.apply(context);return true;}return false;}}

IfSqlNode.png

它会借助 OGNL 完成 test 表达式内容的判断,为 True 则会追加对应 SQL 信息。

接下来继续回到 DynamicSqlSource#getBoundSql 方法,将 #{} 占位符替换为 ? 的逻辑在讲解映射配置时已讲过,不清楚的小伙伴可以再去了解一下,这部分内容没有特别需要关注的,了解下该方法的作用即可:

public class DynamicSqlSource implements SqlSource {// ...@Overridepublic BoundSql getBoundSql(Object parameterObject) {// ...// 处理拼接完成后 SQL 中的 #{} 占位符,将占位符替换为 ?SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();// 解析完成后的 SqlSource 均为 StaticSqlSource 类型,其中记录解析完成后的完整 SQLSqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// StaticSqlSource 获取 BoundSql SQL 的方法就非常简单了:将 SQL 和参数信息记录下来BoundSql boundSql = sqlSource.getBoundSql(parameterObject);// 在 BoundSql 对象中 additionalParameters Map 中添加 key 为 _parameter,value 为入参 的附加参数信息context.getBindings().forEach(boundSql::setAdditionalParameter);return boundSql;}
}

到这里,带有动态标签的 SQL 已被处理成可能带有 ? 占位符的 SQL 字符串了,后续逻辑与上一节中介绍 SQL 的执行流程没有区别,便不再赘述了。接下来我们讨论下 #{} 占位符是如何避免 SQL 注入的问题。

#{} 是如何解决 SQL 注入的?

我们已经了解到 #{} 占位符会被解析成 ?,在 SQL 被执行时,由 JDBC 的 PreparedStatement 将对应的参数会绑定到对应的位置上,它并 不是直接将内容拼接到 SQL 上,注入的 SQL 内容将会 被看作字符串处理,它便是通过这种方式来避免 SQL 注入的。

org.apache.ibatis.session.SqlSessionTest#dynamicTableName 单测为例:

class SqlSessionTest extends BaseDataTest {@Testvoid dynamicTableName() {try (SqlSession session = sqlMapper.openSession()) {AuthorMapper mapper = session.getMapper(AuthorMapper.class);List<Author> author = mapper.selectDynamicTableName("author");assertEquals(2, author.size());}}
}
    <select id="selectDynamicTableName" parameterType="string" resultMap="selectAuthor">select id, username, password, email, bio, favourite_sectionfrom #{tableName}</select>

我们想使用 #{} 占位符动态替换表名,试验下能不能成功,结果控制台打印以下内容:

### SQL: select id, username, password, email, bio, favourite_section from ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''author'' at line 2

发现它将表名参数作为字符串处理,实际执行的 SQL 为:

select id, username, password, email, bio, favourite_section from 'author'

所以任何要注入的 SQL 内容是不能影响到 SQL 语句的,保证了安全性。那么 $ 占位符是如何实现动态 SQL 拼接的呢?我们将 SQL 修改一下:

    <select id="selectDynamicTableName" parameterType="string" resultMap="selectAuthor">select id, username, password, email, bio, favourite_sectionfrom ${tableName}</select>

先前我们提到过,包含 $ 占位符的 SQL 也会被识别为动态 SQL(SqlSource 类型为 DynamicSqlSource),同样我们需要看一下它获取 BoundSql 的逻辑 org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql。在执行该方法时,可以发现整条 SQL 语句被解析为字符串保存在 TextSqlNode 中:

$占位符的解析.png

我们继续看一下 apply 方法的逻辑,发现它会创建一个专门替换 ${} 占位符 GenericTokenParser 解析器:

public class TextSqlNode implements SqlNode {// eg: select id, username, password, email, bio, favourite_section from ${tableName}private final String text;@Overridepublic boolean apply(DynamicContext context) {GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));context.appendSql(parser.parse(text));return true;}private GenericTokenParser createParser(TokenHandler handler) {return new GenericTokenParser("${", "}", handler);}}

这样它在执行 GenericTokenParser#parser 方法时,便会根据上下文信息 ${} 替换成参数直接拼接到 SQL 上,最终 SQL 为:

select id, username, password, email, bio, favourite_section from author

它会直接在原 SQL 上进行拼接,所以会有 SQL 注入的风险,而且我们也能理解包含 ${} 的 SQL 节点被命名为 TextSqlNode 的原因了,Text 便表示 SQL 会被解析为一段 SQL 的文本表达式。

巨人的肩膀

  • 百度百科 - OGNL

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/950229.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数学证明的一些记录

T[2025/10/29]: 题目链接:https://codeforces.com/gym/105578/problem/B \(n与m互质,i从1到n,j从1到m,式子im+jn \% nm\),对于任何一对\(i j\),式子的值都不同 要证明这个式子,即证明不存在一对i,j使得 f(i1,j1)…

10.29(续)

代码大全2的6-10章读后感: 第 6 章 “变量命名的艺术” 看似基础,却直击编程中的 “沟通痛点”。书中强调 “好的命名应能自我说明,让读者无需查看上下文就能理解变量含义”,这一点让我深受触动。以往我常为图方便…

DicomObjects .NET 8.48.231.0 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025.10.29__jyu每日一题题解

完全平方数 题目大意 给定一个正整数 \(n\),找到最小的正整数 x,使得它们的乘积是一个完全平方数。 思路 1. 定理 算术基本定理指出:任何大于1的自然数 \(N\),要么本身是素数,要么可以唯一地分解为有限个素数的乘…

CSP-J/S2024 游记

一个蒟蒻的CSP-J/S2024游记可能是全网写得最晚的一篇 CSP-J/S2024 游记 歌:Fin.ArcDeaR - MisoilePunch♪ ~タケノコ添え~ 游记 Day 0x80000000 在表弟家玩时得知了csp报名的事 申请报名了,但一直没有审核通过 结果…

以《出师表》作为例子,对比通用分块和父子分块的区别

我们以《出师表》(节选)为例,通过具体分割结果对比**通用分块**和**父子分块**的核心差异。《出师表》结构清晰(含表文开头、历史回顾、治国建议、出师目的等部分),适合展示两种分块策略的不同逻辑。以《出师表》…

苏联套娃

无意义文章。无意义文章。

DP 状态设计

如果有些东西是一段一段的,那么你可以考虑一个一个点转移,分别考虑拼到前一段上或者新开一段。 P2679 [NOIP 2015 提高组] 子串

winget不可用,一直转圈,文字变蓝色

情况解决 原因是连接不上默认的winget源。(默认自带winget源和msstore源) 换ustc源之后一切都好了。 https://mirrors.ustc.edu.cn/help/winget-source.html winget source remove winget winget source add winget …

Uno Platform 6.3 发布:支持 .NET 10 预览版并兼容 VS 2026

Uno Platform 6.3 发布,新增对 .NET 10 预览版和 Visual Studio 2026 新解决方案格式的支持,提升 WebAssembly 图像解码性能,优化 TabView 控件和 Hot Design 设计时工具。Uno Platform 团队发布了 6.3 版本,这是一…

申威ky10架构安装MongoDB 4.0.1(rpm包:mongodb-4.0.1-8.ky10.sw_64.rpm)详细步骤

申威ky10架构安装MongoDB 4.0.1(rpm包:mongodb-4.0.1-8.ky10.sw_64.rpm)详细步骤​ ​1. 先检查环境​ 确保你的服务器是申威(ky10)架构的,系统是CentOS/RedHat系的(比如麒麟V10基于ky10的版本)​,因为这rpm包…

线段树入门 - idle

前言 笔者从2025.4.22第一次通过线段树模板,至今也不过半年时间,虽然短暂,但是却让其成为了笔者最喜欢的算法,因此,我常常会大喊我是线段树的狗。为了帮助自己记忆以及造福后人,笔者提键盘写出了这篇文章。——2…

2025年10月临江鳝丝店推荐:五家口碑店铺综合对比排行

临江鳝丝作为乐山地区的特色美食,近年来受到越来越多食客的关注。在选择临江鳝丝店时,消费者通常会考虑店铺的地域特色、食材新鲜度、烹饪技艺以及就餐环境等多个因素。根据餐饮行业数据显示,乐山地区特色餐饮门店数…

文档抽取技术在智能合同对比系统中的应用与优势分析

在商业活动日益频繁的今天,合同作为规范交易、界定权责的核心载体,其审查与管理的重要性不言而喻。传统的合同对比方式主要依赖于法务或业务人员的人工逐字阅读,耗时耗力且容易因疲劳或疏忽导致关键差异被遗漏。随着…

2025年10月临江鳝丝店对比报告:详析五家店铺特色与差异

临江鳝丝作为乐山地区的特色美食,近年来受到越来越多食客的关注。许多游客和本地居民在寻找正宗临江鳝丝店时,常常面临选择困难。根据餐饮行业数据显示,乐山地区主打临江鳝丝的店铺数量在2024年已达到数十家,但品质…

vs2022(2026)离线安装失败的问题解决

安装微软网站上的 创建 Visual Studio 的脱机安装包以进行本地安装 创建离线安装目录后,在内网机器安装一直出错。如果使用--noweb参数,就会提示要下载文件才能安装,但是内网机是不联网的,无法下载。 忘了截图,往…

家训

饿就吃饭菜成什么样了 2156 Div.2 D 压线过了。 考虑从低位到高位判定 0/1,每次 check 的数大约减半,那么 \(time=n+\frac{n}{2}+\frac{n}{4}+\dots=2n+\epsilon\),有 \(\epsilon\) 是因为可能上一步只删了下取整个…

2025年10月临江鳝丝店推荐榜:五家口碑店铺深度对比与选择指南

作为乐山地区特色美食的代表,临江鳝丝以其独特的烹饪工艺和鲜明的地方风味吸引着众多食客。选择一家正宗的临江鳝丝店不仅关乎味蕾享受,更关系到对传统美食文化的体验。当前餐饮市场呈现多元化发展趋势,消费者在选择…

VisionPro学习笔记-CogFixtureTool

CogFixtureTool 定位工具文档 1. 工具概述 CogFixtureTool 是康耐视 VisionPro 视觉软件中的核心坐标系管理工具,主要用于在图像中创建和管理坐标系转换。其核心功能是根据提供的二维变换关系,在图像的原始坐标系(U…

2025年10月临江鳝丝店推荐榜单:五家特色店铺详细对比分析

作为乐山地区最具代表性的江湖菜之一,临江鳝丝近年来受到越来越多美食爱好者的关注。根据餐饮行业数据显示,2025年临江鳝丝类餐厅的搜索量同比增长显著,反映出消费者对这道传统美食的持续热情。许多游客来到乐山,除…