1. MyBatis解析器模块概述
MyBatis 解析器模块是整个框架的“入口门卫”,它的职责是 将开发者编写的 XML 配置文件(全局配置、Mapper 映射文件等)准确地转化为 Java 内部可操作的数据结构。
这一过程看似只是 XML 文件的读取与解析,但实际上 MyBatis 在此过程中引入了多层安全性校验、动态变量替换、离线 DTD 解析、XPath 快速定位等机制。
在深入源码之前,我们先明确:解析器模块是 MyBatis 初始化流程中的第一道关卡,它直接影响后续映射器构建、动态 SQL 生成以及运行时性能。
1.1 核心功能解析
MyBatis 的解析器模块主要由以下几部分功能组成:
-
XML 文件读取与构建 DOM 对象
-
使用
DocumentBuilderFactory
创建DocumentBuilder
,将 XML 文件解析为 W3C DOM 结构。 -
启用了 Secure Processing 特性,防止 XML 外部实体注入(XXE 攻击)。
-
支持 UTF-8、UTF-16 等多种字符编码。
-
-
XPath 定位与节点提取
-
借助
XPathParser
对 DOM 进行快速节点查找。 -
支持通过 XPath 表达式(如
/configuration/mappers/mapper
)直接定位所需节点,避免手动 DOM 遍历。
-
-
本地化 DTD/XSD 验证
-
使用
XMLMapperEntityResolver
在本地查找 MyBatis 自带的 DTD/XSD 文件,避免联网下载,提高解析速度。 -
支持离线模式(无网络环境下依旧可解析 Mapper 文件)。
-
-
动态变量替换
-
通过
PropertyParser
和GenericTokenParser
协同工作,将 XML 配置中的${}
动态占位符替换为实际值。 -
这种替换在 XML 转换为内部数据结构之前 进行,确保后续配置加载的一致性。
-
-
占位符与参数安全处理
-
解析 SQL 中的
${}
(直接文本替换)和#{}
(预编译参数绑定)两种占位符。 -
在解析阶段区分两者,防止 SQL 注入风险。
-
1.1.1 XPath封装机制
XPath 在 MyBatis 中的应用核心是 XPathParser
类。
虽然 Java 原生 XPathFactory
和 XPath
API 已经足够强大,但 MyBatis 做了二次封装,主要为了:
-
简化调用
-
提供
evalNode
、evalString
、evalNodes
等方法,一行代码即可定位节点或获取属性值。 -
避免开发者手动编写
XPathExpression
、evaluate
这样的低层 API。
-
-
变量替换集成
-
内部在执行 XPath 前,会先对节点文本内容进行变量替换(基于
Variables
属性)。 -
支持多环境(environment)下的动态切换。
-
-
命名空间支持
-
MyBatis 在创建
XPathFactory
时开启了命名空间处理(setNamespaceAware(true)
)。 -
在解析 Mapper XML 时,能够正确处理
<mapper namespace="...">
这样的属性。
-
示例代码(获取 MyBatis 配置文件中的所有 mapper 节点):
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
XPathParser parser = new XPathParser(inputStream, true, null, new XMLMapperEntityResolver());
List<XNode> mapperNodes = parser.evalNodes("/configuration/mappers/mapper");
for (XNode node : mapperNodes) {
System.out.println("Mapper resource: " + node.getStringAttribute("resource"));
}
这里的 evalNodes
会返回一组 XNode
对象,每个 XNode
都是 MyBatis 对 org.w3c.dom.Node
的轻量封装,附带便捷的属性读取方法。
1.1.2 本地DTD/XSD解析策略
XML 文档中通常会声明 DTD 或 XSD,例如:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
如果解析器每次都去 http://mybatis.org
下载 DTD,将严重拖慢启动速度,并在无网络环境中直接失败。
MyBatis 的解决方案是:使用 XMLMapperEntityResolver
拦截实体解析请求,将 URL 替换成本地资源路径。
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
if (systemId != null) {
if (systemId.contains("mybatis-3-mapper.dtd")) {
return new InputSource(
XMLMapperEntityResolver.class.getResourceAsStream("/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd")
);
}
}
return null;
}
这样,即使完全离线,MyBatis 依然能正常解析 Mapper XML。
1.2 解析器模块在MyBatis初始化中的角色
MyBatis 的初始化流程大致如下:
-
创建
SqlSessionFactoryBuilder
-
读取 MyBatis 全局配置文件(mybatis-config.xml)
-
XMLConfigBuilder
使用XPathParser
解析全局配置 -
加载
<mappers>
配置,逐个解析 Mapper XML -
生成 MapperStatement、ResultMap、Cache 等对象,注册到 Configuration
-
返回构建好的
SqlSessionFactory
在这个过程中,解析器模块的工作集中在第 2~4 步,即:
-
把 XML 转换成 DOM 树。
-
使用 XPath 快速找到所需的节点(如
<environments>
、<mappers>
)。 -
进行变量替换、DTD/XSD 验证。
-
把节点信息转化为 MyBatis 内部的 Java 对象。
调用链示例(简化版):
SqlSessionFactoryBuilder.build(...)
└── XMLConfigBuilder.parse()
└── XPathParser.evalNode("/configuration")
├── Variables替换
├── DTD本地化校验
└── XNode封装
解析器模块是 MyBatis 所有 XML 配置加载的“总入口”,
一旦解析失败(如 DTD 校验不通过、变量缺失等),MyBatis 会在初始化阶段直接抛出异常,阻止应用启动。
2. 模块架构与核心组件
MyBatis 解析器模块并不是一个单独的 package,而是分布在 org.apache.ibatis.parsing
、org.apache.ibatis.builder.xml
等包中,由多个协作类组成。它们协同完成 XML 读取、节点定位、变量替换以及 DTD/XSD 验证等任务。
2.1 模块整体架构描述(文字版架构图)
如果用文字来画出模块的分层结构,可以这样表示:
[输入层]
├── mybatis-config.xml(全局配置文件)
└── Mapper XML 文件(SQL 映射配置)
[解析入口层]
├── XMLConfigBuilder(全局配置解析器)
└── XMLMapperBuilder(Mapper 文件解析器)
[解析核心层]
├── XPathParser(XPath 封装解析器)
│ ├── 依赖 javax.xml.xpath.XPath
│ ├── 集成 Variables 替换
│ └── 提供 eval 系列方法
├── XMLMapperEntityResolver(本地 DTD/XSD 解析器)
│ └── 拦截外部实体请求,映射到本地资源
├── PropertyParser(属性替换解析器)
│ └── 负责替换 ${} 占位符
└── GenericTokenParser(通用 Token 解析器)
└── 提供基于起止符的通用替换逻辑
[输出层]
└── Configuration 对象(MyBatis 核心运行时配置)
流程文字版描述:
-
入口层:
XMLConfigBuilder
负责读取mybatis-config.xml
,XMLMapperBuilder
负责解析每一个 Mapper 文件。 -
核心层:
-
XPathParser:对 XML DOM 进行 XPath 定位,封装了
evalNode
、evalString
等方法。 -
XMLMapperEntityResolver:本地化 DTD/XSD 校验,避免联网下载。
-
PropertyParser + GenericTokenParser:处理
${}
占位符替换。
-
-
输出层:解析完成后,将配置项注册到
Configuration
对象,供 MyBatis 运行时使用。
2.2 核心类UML关系分析(文字版UML)
用文字来画 UML 关系,可以这样表示:
+---------------------+ uses +-----------------------+
| XMLConfigBuilder | -----------------> | XPathParser |
+---------------------+ +-----------------------+
| |
| uses | uses
v v
+---------------------+ +-----------------------+
| XMLMapperBuilder | -----------------> | XMLMapperEntityResolver|
+---------------------+ +-----------------------+
|
| uses
v
+---------------------+ collaborates +-----------------------+
| PropertyParser | <--------------> | GenericTokenParser |
+---------------------+ +-----------------------+
-
XMLConfigBuilder / XMLMapperBuilder
-
高层解析器,调用
XPathParser
进行节点查找。
-
-
XPathParser
-
持有
Variables
属性(Map),在解析节点值时进行变量替换。 -
依赖
XMLMapperEntityResolver
进行本地 DTD/XSD 解析。
-
-
PropertyParser
-
逻辑层,只负责占位符的替换规则。
-
-
GenericTokenParser
-
工具层,实现
${}
、#{}
等基于 token 的解析算法。
-
这种设计模式的好处:
-
高层类只关心“我要什么节点”,不关心 XML 如何加载、DTD 如何处理。
-
低层类职责单一,方便单元测试和扩展(例如可自定义 EntityResolver)。
2.3 XML解析方式对比(DOM/SAX/StAX)
MyBatis 选择了 DOM + XPath 的组合来解析 XML,原因是其配置文件和 Mapper 文件规模相对可控,而且节点访问频繁,需要随机访问。
解析方式 | 特点 | 优点 | 缺点 | MyBatis 是否使用 |
---|---|---|---|---|
DOM | 一次性将整个 XML 载入内存,生成树形结构 | 方便随机访问,支持 XPath | 内存占用大,启动时耗时 | ✅ |
SAX | 基于事件流的顺序解析 | 占用内存小,速度快 | 只能顺序访问,需手动维护状态 | ❌ |
StAX | 基于游标的流式解析 | 内存可控,可读可写 | API 相对复杂 | ❌ |
为什么 MyBatis 选 DOM + XPath?
-
配置文件读取频率低(只在启动时加载一次),启动性能不是瓶颈。
-
配置文件节点访问频繁(需要多次获取同一节点、属性)。
-
XPath 高效定位:相比手动 DOM 遍历,XPath 语法简洁、可读性强。
示例:用 XPath 定位所有 <mapper>
节点(MyBatis 内部类似用法):
XPathParser parser = new XPathParser(inputStream, true, variables, new XMLMapperEntityResolver());
List<XNode> mappers = parser.evalNodes("/configuration/mappers/mapper");
这段代码相比原生 DOM 遍历要短得多,并且避免了节点深度硬编码。
3. 核心类源码解析
解析器模块的“发动机”就是几个核心类:XPathParser
、XMLMapperEntityResolver
、PropertyParser
、GenericTokenParser
。
它们分别负责 XPath 定位、DTD/XSD 本地化解析、变量替换、通用占位符解析,协同完成 MyBatis 的 XML 解析任务。
3.1 XPathParser 类详解
XPathParser
是 MyBatis 对 Java 原生 javax.xml.xpath.XPath
API 的封装。它不仅封装了节点查找,还集成了变量替换、DTD 验证等功能。
3.1.1 字段初始化流程
源码位置:org.apache.ibatis.parsing.XPathParser
核心字段:
private final Document document;
private boolean validation;
private EntityResolver entityResolver;
private Properties variables;
private XPath xpath;
-
document
-
存放解析后的 DOM 树。
-
-
validation
-
是否进行 DTD/XSD 校验。
-
-
entityResolver
-
实际使用的是
XMLMapperEntityResolver
,拦截外部实体请求。
-
-
variables
-
存放
${}
变量的值映射。
-
-
xpath
-
Java 原生 XPath 对象,用于表达式计算。
-
初始化流程:
-
读取输入流(XML 文件)。
-
调用
createDocument
方法创建 DOM 树。 -
保存
variables
属性,用于后续节点值替换。 -
初始化
XPathFactory
并创建XPath
对象。
3.1.2 构造方法调用链
常用构造方法:
public XPathParser(InputStream inputStream, boolean validation,
Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(inputStream));
}
调用链说明:
-
commonConstructor
-
保存
validation
、variables
、entityResolver
。 -
初始化
XPathFactory
:XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath();
-
开启命名空间支持(在 XML 构建时设置)。
-
-
createDocument
-
使用
DocumentBuilderFactory
创建DocumentBuilder
。 -
开启 Secure Processing:
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
防止外部实体注入(XXE 攻击)。
-
设置验证模式:
factory.setValidating(validation); factory.setNamespaceAware(true);
-
绑定
entityResolver
,实现 DTD/XSD 本地解析。
-
3.1.3 eval
系列方法
XPathParser
提供多种 eval
方法,返回值类型不同:
public XNode evalNode(String expression) { ... }
public List<XNode> evalNodes(String expression) { ... }
public String evalString(String expression) { ... }
public Boolean evalBoolean(String expression) { ... }
public Integer evalInteger(String expression) { ... }
调用示例(解析 <environments>
节点):
XPathParser parser = new XPathParser(inputStream, true, vars, new XMLMapperEntityResolver());
XNode environments = parser.evalNode("/configuration/environments");
String defaultEnv = environments.getStringAttribute("default");
注意:
-
在返回
XNode
之前,会先执行变量替换:PropertyParser.parse(value, variables);
-
evalNodes
会返回多个匹配的节点(List),而evalNode
只取第一个。
3.1.4 实际应用场景
XPathParser
在 MyBatis 中被广泛使用,例如:
-
XMLConfigBuilder
解析全局配置文件。 -
XMLMapperBuilder
解析 Mapper 文件中的<select>
、<insert>
等 SQL 语句。 -
XMLStatementBuilder
解析具体 SQL 节点。
示例:Mapper 文件解析
XPathParser parser = new XPathParser(mapperInputStream, true, vars, new XMLMapperEntityResolver());
XNode mapperNode = parser.evalNode("/mapper");
String namespace = mapperNode.getStringAttribute("namespace");
3.2 XMLMapperEntityResolver详解
XMLMapperEntityResolver
负责将 XML 中声明的 DTD/XSD 路径映射到本地文件,避免联网解析。
源码位置:org.apache.ibatis.builder.xml.XMLMapperEntityResolver
核心方法:
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
if (systemId.contains("mybatis-3-mapper.dtd")) {
return getInputSource("/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd", publicId, systemId);
} else if (systemId.contains("mybatis-3-config.dtd")) {
return getInputSource("/org/apache/ibatis/builder/xml/mybatis-3-config.dtd", publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e);
}
}
-
本地化解析:如果
systemId
包含mybatis-3-mapper.dtd
,则直接读取 jar 包内的资源文件。 -
离线支持:无网络时依然可以加载 DTD 文件。
示例:在无网络环境中解析 Mapper
XPathParser parser = new XPathParser(inputStream, true, variables, new XMLMapperEntityResolver());
即使 <!DOCTYPE mapper ...>
指向远程 URL,也不会触发网络请求。
3.3 PropertyParser与GenericTokenParser协作机制(约1200字)
3.3.1 Token解析过程
GenericTokenParser
是一个通用的占位符解析工具,它不关心具体业务,只负责:
-
按起始符和结束符切分字符串。
-
调用回调处理器处理 token 内容。
-
拼接替换后的结果。
源码简化版:
public String parse(String text) {
int start = text.indexOf(openToken);
if (start == -1) return text;
StringBuilder builder = new StringBuilder();
while (start > -1) {
int end = text.indexOf(closeToken, start);
String content = text.substring(start + openToken.length(), end);
builder.append(handler.handleToken(content));
start = text.indexOf(openToken, end);
}
return builder.toString();
}
3.3.2 动态变量替换与安全性处理
PropertyParser
是业务层实现,使用 GenericTokenParser
来替换 ${}
占位符:
GenericTokenParser parser = new GenericTokenParser("${", "}", content -> {
return variables.getProperty(content, "");
});
return parser.parse(stringValue);
-
${}
占位符会在 解析 XML 时直接替换 成实际值。 -
#{}
占位符则保留到 SQL 执行阶段,使用 JDBC PreparedStatement 绑定参数,防止 SQL 注入。
示例:
假设 MyBatis 配置文件中有:
<property name="db.url" value="jdbc:mysql://localhost:3306/test"/>
<dataSource url="${db.url}" ... />
解析时 ${db.url}
会被替换为实际 JDBC URL。
4. 配置文件与 Mapper 解析流程
4.1 配置文件解析总体流程
MyBatis 在启动时会加载核心配置文件(mybatis-config.xml
)与多个 Mapper XML 文件(*Mapper.xml
),最终将所有信息封装到 Configuration
对象中。这一步由 XMLConfigBuilder
、XMLMapperBuilder
等解析器类完成,核心依赖 XPathParser
来完成 XML 节点的定位和内容读取。
整体流程可以分为两大阶段:
-
全局配置文件解析阶段
-
解析环境配置(
<environments>
) -
解析类型别名(
<typeAliases>
) -
解析插件(
<plugins>
) -
解析 Mapper 注册(
<mappers>
)
-
-
Mapper 文件解析阶段
-
解析
<resultMap>
(结果映射) -
解析
<sql>
(可复用 SQL 片段) -
解析
<select>
、<insert>
、<update>
、<delete>
(SQL 语句) -
处理动态 SQL 标签(
<if>
、<foreach>
、${}
、#{}
)
-
文字版架构图:
启动入口(SqlSessionFactoryBuilder.build)
→ XMLConfigBuilder.parse()
→ XPathParser 读取 mybatis-config.xml
→ 逐步调用各标签解析方法
→ 解析 mappers
→ XMLMapperBuilder.parse()
→ XPathParser 读取 Mapper XML
→ 解析 resultMap / sql / select 等
4.2 全局配置文件解析
4.2.1 XMLConfigBuilder 的职责
XMLConfigBuilder
是 MyBatis 读取核心配置文件的入口类。它内部持有一个 XPathParser
实例来进行 XML 节点的检索。
核心方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里的 parser.evalNode("/configuration")
就是用 XPath 精确定位到根节点 <configuration>
。
4.2.2 各标签解析
示例:
<configuration>
<typeAliases>
<typeAlias alias="User" type="com.example.domain.User"/>
</typeAliases>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
解析逻辑:
-
<typeAliases>
→ 调用typeAliasesElement()
方法,批量注册别名到TypeAliasRegistry
。 -
<mappers>
→ 调用mapperElement()
方法,逐个注册 Mapper。
源码片段(mapperElement()
):
private void mapperElement(XNode parent) throws Exception {
for (XNode child : parent.getChildren()) {
String resource = child.getStringAttribute("resource");
if (resource != null) {
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
}
}
重点:这里的 XMLMapperBuilder
会启动第二阶段的 Mapper 解析。
4.3 Mapper XML 解析
4.3.1 XMLMapperBuilder 角色
XMLMapperBuilder
负责解析单个 Mapper 文件,关键方法是:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
}
流程:
-
定位
<mapper>
根节点 -
调用
configurationElement()
解析子标签 -
将解析结果注册到
Configuration
对象
4.3.2 解析 <resultMap>
示例:
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</resultMap>
处理:
-
生成
ResultMap
对象 -
通过
ResultMapping
保存列到属性的映射关系 -
支持嵌套映射、鉴别器等高级特性
4.3.3 解析 <select>
等 SQL 标签
示例:
<select id="selectUser" parameterType="int" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
流程:
-
调用
buildStatementFromContext()
将<select>
转换为MappedStatement
-
创建
SqlSource
(静态或动态) -
动态 SQL 则由
XMLScriptBuilder
解析标签(<if>
、<foreach>
等)
4.4 XPathParser 在解析流程中的作用
在以上两个阶段,XPathParser
都是核心工具类,用于:
-
定位 XML 节点(
evalNode
、evalNodes
) -
读取属性(
getStringAttribute
) -
支持变量替换(
variables
)
例如:
XNode context = parser.evalNode("/mapper/select");
String id = context.getStringAttribute("id");
4.5 无网络环境下的 DTD/XSD 解析方案
如果不做处理,MyBatis 在解析 XML 时会访问网络获取 DTD/XSD 文件,导致启动缓慢或失败。
解决方案:
-
使用
XMLMapperEntityResolver
拦截resolveEntity
请求 -
将本地 DTD/XSD 映射到类路径下的文件
示例:
public InputSource resolveEntity(String publicId, String systemId) {
if (systemId != null && systemId.contains("mybatis-3-mapper.dtd")) {
return new InputSource(getClass().getResourceAsStream("/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd"));
}
return null;
}
4.6 性能优化建议
-
预编译 XPath 表达式
避免重复解析相同路径,提高大文件解析效率。 -
减少映射文件数量
将多个 SQL 放在一个 Mapper 中,减少 I/O 次数。 -
禁用无用的 XML 校验
如果在本地调试且 XML 保证正确,可以关闭严格校验以减少启动时间。
4.7 常见错误与排查方法
-
Document is invalid
→ 检查 XML 头部声明和 DTD/XSD 是否匹配。 -
BuilderException: Error parsing Mapper XML
→ 检查 SQL 标签是否在<mapper>
下,标签闭合是否正确。 -
Could not resolve entity
→ 配置XMLMapperEntityResolver
以使用本地 DTD/XSD。
MyBatis Mapper XML 解析调用链(文字版)
[MyBatis 初始化]
↓
SqlSessionFactoryBuilder.build(InputStream config)
↓
XMLConfigBuilder.parse() // 解析 mybatis-config.xml 主配置
↓
Configuration.addMapper(Class<?> type) // 注册 Mapper 接口
↓
MapperRegistry.addMapper(Class<?> type)
↓
MapperAnnotationBuilder.parse() ← 注解方式解析(如果存在注解)
↓
XMLMapperBuilder.parse() // 解析对应 Mapper XML 文件
↓
XMLMapperBuilder.configurationElement(XNode context) // 解析 <mapper> 根节点
├─> parseCacheRef()
├─> parseCache()
├─> parseResultMaps()
├─> parseSqlFragments()
└─> parseStatement() // 解析 <select> <insert> <update> <delete>
↓
XMLStatementBuilder.parseStatementNode()
↓
LanguageDriver.createSqlSource() // 动态 SQL 解析入口
↓
XMLScriptBuilder.parseScriptNode()
↓
MixedSqlNode.apply(DynamicContext context)
↓
SqlNode 组合解析(StaticTextSqlNode、IfSqlNode、ChooseSqlNode等)
↓
GenericTokenParser.parse() // 处理 #{} 和 ${} 占位符
↓
ParameterMappingTokenHandler // 生成参数映射信息
↓
SqlSource.build() // 生成 BoundSql
↓
MappedStatement 对象创建完成并存入 Configuration.mappedStatements
解析链条分解说明
-
XMLConfigBuilder:负责主配置文件解析,找到
<mappers>
节点并触发 Mapper 文件解析。 -
XMLMapperBuilder:专门处理 Mapper XML 文件,拆分为多个部分(缓存、结果映射、SQL 片段、SQL 语句)。
-
XMLStatementBuilder:针对单条 SQL 标签进行解析。
-
LanguageDriver / XMLScriptBuilder:解析动态 SQL 节点。
-
GenericTokenParser:最终解析占位符,区分
${}
(字符串替换)与#{}
(安全参数绑定)。
第 5 章 动态 SQL 占位符处理机制
5.1 ${} 与 #{} 的区别与应用场景
在 MyBatis 中,${}
与 #{}
虽然都可以用来传递参数,但它们的底层处理方式与 SQL 安全性差别极大。
-
#{}
占位符-
作用:将传入的参数预编译绑定到 SQL 中。
-
处理方式:会被 MyBatis 转换成 JDBC 的
?
占位符,然后通过PreparedStatement.setXXX()
方法安全传值。 -
优点:
-
防止 SQL 注入(因为参数不会直接拼接到 SQL 中)。
-
JDBC 会自动进行类型转换。
-
-
典型场景:
<select id="findUserById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select>
-
适合条件查询、插入、更新等需要参数绑定的场景。
-
-
-
${}
占位符-
作用:字符串替换,直接把值拼接到 SQL 中。
-
处理方式:在 SQL 解析阶段直接替换为字符串,不经过 JDBC 预编译。
-
缺点:
-
容易引发 SQL 注入风险(如果值来自用户输入且未经过过滤)。
-
无法自动进行类型转换,需要开发者保证值的正确性。
-
-
典型场景:
<select id="findUserByTable" resultType="User"> SELECT * FROM ${tableName} WHERE status = 1 </select>
-
适合动态表名、动态列名等场景,但必须严格控制输入来源。
-
-
-
对比总结
特性 #{} ${} SQL注入防护 有 无 类型转换 自动 无 性能 高(预编译) 略低(字符串拼接) 使用场景 普通参数传递 动态结构(表名、列名)
5.2 占位符解析源码分析
在 Mapper XML 的解析过程中,MyBatis 使用 GenericTokenParser
作为通用占位符解析器,同时结合 ParameterMappingTokenHandler
或 VariableTokenHandler
来完成 ${}
与 #{}
的具体替换。
调用链核心如下:
XMLScriptBuilder.parseScriptNode()
↓
MixedSqlNode.apply(DynamicContext context)
↓
GenericTokenParser.parse()
↓
TokenHandler.handleToken()
#{} 的处理流程
-
XMLStatementBuilder.parseStatementNode()
-
获取 SQL 节点内容,进入
LanguageDriver
。
-
-
XMLScriptBuilder
-
创建
MixedSqlNode
,包含多个SqlNode
(动态节点、文本节点等)。
-
-
GenericTokenParser
-
初始化时指定:
-
起始标记:
#{
-
结束标记:
}
-
处理器:
ParameterMappingTokenHandler
-
-
-
ParameterMappingTokenHandler
-
根据占位符内容(如
id
、user.name
)创建ParameterMapping
对象。 -
返回
?
作为 SQL 占位符。
-
源码片段(简化版):
public String handleToken(String content) {
Object value = context.getBindings().get(content);
return value == null ? "" : value.toString();
}
${} 的处理流程
-
同样由
GenericTokenParser
解析,但使用VariableTokenHandler
。 -
VariableTokenHandler
-
直接从上下文变量中获取值并替换。
-
-
无参数映射创建,直接返回拼接值。
源码片段(简化版):
public String handleToken(String content) {
Object value = context.getBindings().get(content);
return value == null ? "" : value.toString();
}
5.3 安全性与性能优化建议
-
安全性建议
-
避免在 SQL 中直接使用
${}
处理用户输入。 -
如果必须使用
${}
(动态表名、列名),应在代码中进行白名单校验:List<String> tables = Arrays.asList("user", "order"); if (!tables.contains(tableName)) { throw new IllegalArgumentException("非法表名"); }
-
对字符串值进行转义,防止拼接引发注入。
-
-
性能优化
-
尽量使用
#{}
,利用 JDBC 预编译的缓存能力,减少 SQL 解析与编译开销。 -
对重复执行的动态 SQL,可以通过缓存
BoundSql
提升效率。 -
复杂动态 SQL(多层
<if>
、<choose>
)应考虑在业务层拼接完毕后交给 MyBatis,减少运行时解析负担。
-
-
调试与排错
-
启用 MyBatis 日志输出 SQL 及参数绑定信息:
<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>
-
检查最终 SQL 与参数是否匹配。
-
使用
BoundSql.getSql()
与BoundSql.getParameterMappings()
进行程序内调试。
-
-
最佳实践总结
-
数据值用 #{}:安全、性能高。
-
结构值用 ${}:必须验证来源,防止注入。
-
混合使用时,先拼结构再绑定数据,避免全拼接。
-
6. 性能优化与最佳实践
6.1 MyBatis XML 解析性能瓶颈分析
MyBatis 的解析器模块主要负责读取和解析配置文件与 Mapper XML 文件,完成 SQL 语句的动态构建。这一阶段虽然只在应用启动或 Mapper 初始化时执行,但对于大型项目和高并发场景,优化解析性能同样至关重要。
6.1.1 主要性能瓶颈
-
XML 文件加载与校验
-
MyBatis 默认使用 JAXP 的 DOM 解析器(
DocumentBuilderFactory
)加载配置与 Mapper 文件。DOM 解析会将整个 XML 文件加载进内存,随着 XML 文件体积增长,内存压力增加。 -
配置文件的 DTD 或 XSD 校验如果频繁触发,会增加解析时间。
-
-
动态 SQL 解析
-
Mapper 中大量使用
<if>
,<choose>
,<foreach>
等动态标签时,解析树的构建与执行需要占用 CPU。
-
-
占位符解析
-
GenericTokenParser
处理动态参数占位符时,重复解析耗时。
-
-
重复解析
-
如果未有效缓存解析结果,重复解析同一 Mapper 文件会浪费资源。
-
6.2 优化方案与实践
6.2.1 关闭不必要的 XML 校验
默认情况下,DocumentBuilderFactory
会开启 XML Schema 或 DTD 校验。对于已确定无误的配置文件,关闭校验能节省大量时间。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
-
关闭外部 DTD 加载,避免网络阻塞。
-
关闭校验后仍然保证 XML 结构完整。
6.2.2 启用 DocumentBuilderFactory 的 Secure Processing
MyBatis 默认设置 DocumentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
,这避免了 XML 外部实体攻击,也间接提升了性能。
6.2.3 缓存 Mapper 解析结果
MyBatis 默认将 MappedStatement
等解析结果缓存于 Configuration
对象中,避免重复解析。
建议:
-
避免在运行时动态修改 Mapper XML。
-
通过合理的模块拆分,避免单个 Mapper 过大。
6.2.4 使用 StAX 解析器的可能性
-
DOM 解析加载整个 XML 到内存,适合配置文件不大时使用。
-
StAX(Streaming API for XML)支持基于事件的逐步读取,适合大文件解析,有更小内存占用和更快解析速度。
-
MyBatis 源码默认未使用 StAX,但可以通过自定义解析器实现替换。
6.3 动态 SQL 的性能优化建议
-
减少复杂的动态标签嵌套
-
深度嵌套的
<if>
、<choose>
标签会增加解析复杂度。
-
-
提前在业务层处理复杂逻辑
-
将复杂的动态逻辑处理迁移到 Java 代码中,传递简单参数给 Mapper。
-
-
SqlNode 缓存机制
-
MyBatis 会缓存 SqlNode 树,但动态内容会在每次调用时计算,减少无用的动态节点。
-
6.4 动态占位符安全性最佳实践
-
避免不安全的
${}
占位符-
强制白名单校验,过滤非法参数。
-
-
优先使用
#{}
占位符-
JDBC 预编译参数,防止 SQL 注入。
-
-
输入参数过滤与校验
-
对所有来自用户的参数进行类型校验与转义。
-
-
日志监控
-
开启 MyBatis SQL 日志,及时发现异常 SQL。
-
6.5 常见配置错误与排查技巧
错误场景 | 现象 | 解决方案 |
---|---|---|
XML 文件格式错误 | 解析异常,启动失败 | 使用 XML 编辑器校验格式,关闭 DTD 校验试错 |
Mapper 文件路径配置错误 | Mapper 未被加载,方法找不到 | 检查 <mappers> 节点路径是否正确 |
动态 SQL 占位符未定义参数 | 报错或拼接错误 | 检查参数名称是否与 Mapper 方法匹配 |
XML 校验失败 | 抛出 SAXParseException | 检查 DTD 或 XSD 版本,关闭校验排查 |
多环境配置冲突 | 配置覆盖导致参数异常 | 统一管理配置文件,避免冲突 |
6.6 代码示例:关闭外部 DTD 加载与安全处理
public Document createSecureDocument(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// 禁止加载外部DTD,防止网络延迟和攻击
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(new XMLMapperEntityResolver()); // 使用本地实体解析器
return builder.parse(inputSource);
}
6.7 结合实际项目场景的性能优化建议
-
无网络环境下的 DTD 解析
-
使用
XMLMapperEntityResolver
解析器,避免外部网络请求导致启动缓慢或失败。
-
-
大型项目拆分 Mapper
-
按模块拆分 Mapper,减少单文件大小,提升解析速度。
-
-
自定义 EntityResolver 优化
-
针对企业环境的本地缓存机制,减少实体解析时间。
-
-
监控与预警
-
结合 APM 工具监控解析阶段耗时,针对热点 Mapper 进行优化。
-