MyBatis 解析器模块全景剖析

1. MyBatis解析器模块概述

MyBatis 解析器模块是整个框架的“入口门卫”,它的职责是 将开发者编写的 XML 配置文件(全局配置、Mapper 映射文件等)准确地转化为 Java 内部可操作的数据结构
这一过程看似只是 XML 文件的读取与解析,但实际上 MyBatis 在此过程中引入了多层安全性校验、动态变量替换、离线 DTD 解析、XPath 快速定位等机制。

在深入源码之前,我们先明确:解析器模块是 MyBatis 初始化流程中的第一道关卡,它直接影响后续映射器构建、动态 SQL 生成以及运行时性能。


1.1 核心功能解析

MyBatis 的解析器模块主要由以下几部分功能组成:

  1. XML 文件读取与构建 DOM 对象

    • 使用 DocumentBuilderFactory 创建 DocumentBuilder,将 XML 文件解析为 W3C DOM 结构。

    • 启用了 Secure Processing 特性,防止 XML 外部实体注入(XXE 攻击)。

    • 支持 UTF-8、UTF-16 等多种字符编码。

  2. XPath 定位与节点提取

    • 借助 XPathParser 对 DOM 进行快速节点查找。

    • 支持通过 XPath 表达式(如 /configuration/mappers/mapper)直接定位所需节点,避免手动 DOM 遍历。

  3. 本地化 DTD/XSD 验证

    • 使用 XMLMapperEntityResolver 在本地查找 MyBatis 自带的 DTD/XSD 文件,避免联网下载,提高解析速度。

    • 支持离线模式(无网络环境下依旧可解析 Mapper 文件)。

  4. 动态变量替换

    • 通过 PropertyParserGenericTokenParser 协同工作,将 XML 配置中的 ${} 动态占位符替换为实际值。

    • 这种替换在 XML 转换为内部数据结构之前 进行,确保后续配置加载的一致性。

  5. 占位符与参数安全处理

    • 解析 SQL 中的 ${}(直接文本替换)和 #{}(预编译参数绑定)两种占位符。

    • 在解析阶段区分两者,防止 SQL 注入风险。


1.1.1 XPath封装机制

XPath 在 MyBatis 中的应用核心是 XPathParser 类。
虽然 Java 原生 XPathFactoryXPath API 已经足够强大,但 MyBatis 做了二次封装,主要为了:

  1. 简化调用

    • 提供 evalNodeevalStringevalNodes 等方法,一行代码即可定位节点或获取属性值。

    • 避免开发者手动编写 XPathExpressionevaluate 这样的低层 API。

  2. 变量替换集成

    • 内部在执行 XPath 前,会先对节点文本内容进行变量替换(基于 Variables 属性)。

    • 支持多环境(environment)下的动态切换。

  3. 命名空间支持

    • 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 的初始化流程大致如下:

  1. 创建 SqlSessionFactoryBuilder

  2. 读取 MyBatis 全局配置文件(mybatis-config.xml)

  3. XMLConfigBuilder 使用 XPathParser 解析全局配置

  4. 加载 <mappers> 配置,逐个解析 Mapper XML

  5. 生成 MapperStatement、ResultMap、Cache 等对象,注册到 Configuration

  6. 返回构建好的 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.parsingorg.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 核心运行时配置)

流程文字版描述:

  1. 入口层XMLConfigBuilder 负责读取 mybatis-config.xmlXMLMapperBuilder 负责解析每一个 Mapper 文件。

  2. 核心层

    • XPathParser:对 XML DOM 进行 XPath 定位,封装了 evalNodeevalString 等方法。

    • XMLMapperEntityResolver:本地化 DTD/XSD 校验,避免联网下载。

    • PropertyParser + GenericTokenParser:处理 ${} 占位符替换。

  3. 输出层:解析完成后,将配置项注册到 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. 核心类源码解析

解析器模块的“发动机”就是几个核心类:XPathParserXMLMapperEntityResolverPropertyParserGenericTokenParser
它们分别负责 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 对象,用于表达式计算。

初始化流程:

  1. 读取输入流(XML 文件)。

  2. 调用 createDocument 方法创建 DOM 树。

  3. 保存 variables 属性,用于后续节点值替换。

  4. 初始化 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));
}

调用链说明:

  1. commonConstructor

    • 保存 validationvariablesentityResolver

    • 初始化 XPathFactory

      XPathFactory factory = XPathFactory.newInstance();
      this.xpath = factory.newXPath();
      
    • 开启命名空间支持(在 XML 构建时设置)。

  2. 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 对象中。这一步由 XMLConfigBuilderXMLMapperBuilder 等解析器类完成,核心依赖 XPathParser 来完成 XML 节点的定位和内容读取。

整体流程可以分为两大阶段:

  1. 全局配置文件解析阶段

    • 解析环境配置(<environments>

    • 解析类型别名(<typeAliases>

    • 解析插件(<plugins>

    • 解析 Mapper 注册(<mappers>

  2. 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>

解析逻辑:

  1. <typeAliases> → 调用 typeAliasesElement() 方法,批量注册别名到 TypeAliasRegistry

  2. <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();
    }
}

流程:

  1. 定位 <mapper> 根节点

  2. 调用 configurationElement() 解析子标签

  3. 将解析结果注册到 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>

流程:

  1. 调用 buildStatementFromContext()<select> 转换为 MappedStatement

  2. 创建 SqlSource(静态或动态)

  3. 动态 SQL 则由 XMLScriptBuilder 解析标签(<if><foreach> 等)


4.4 XPathParser 在解析流程中的作用

在以上两个阶段,XPathParser 都是核心工具类,用于:

  • 定位 XML 节点(evalNodeevalNodes

  • 读取属性(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 性能优化建议

  1. 预编译 XPath 表达式
    避免重复解析相同路径,提高大文件解析效率。

  2. 减少映射文件数量
    将多个 SQL 放在一个 Mapper 中,减少 I/O 次数。

  3. 禁用无用的 XML 校验
    如果在本地调试且 XML 保证正确,可以关闭严格校验以减少启动时间。


4.7 常见错误与排查方法

  1. Document is invalid → 检查 XML 头部声明和 DTD/XSD 是否匹配。

  2. BuilderException: Error parsing Mapper XML → 检查 SQL 标签是否在 <mapper> 下,标签闭合是否正确。

  3. 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 安全性差别极大。

  1. #{} 占位符

    • 作用:将传入的参数预编译绑定到 SQL 中。

    • 处理方式:会被 MyBatis 转换成 JDBC 的 ? 占位符,然后通过 PreparedStatement.setXXX() 方法安全传值。

    • 优点:

      • 防止 SQL 注入(因为参数不会直接拼接到 SQL 中)。

      • JDBC 会自动进行类型转换。

    • 典型场景:

      <select id="findUserById" resultType="User">
          SELECT * FROM user WHERE id = #{id}
      </select>
      
      • 适合条件查询、插入、更新等需要参数绑定的场景。

  2. ${} 占位符

    • 作用:字符串替换,直接把值拼接到 SQL 中。

    • 处理方式:在 SQL 解析阶段直接替换为字符串,不经过 JDBC 预编译。

    • 缺点:

      • 容易引发 SQL 注入风险(如果值来自用户输入且未经过过滤)。

      • 无法自动进行类型转换,需要开发者保证值的正确性。

    • 典型场景:

      <select id="findUserByTable" resultType="User">
          SELECT * FROM ${tableName} WHERE status = 1
      </select>
      
      • 适合动态表名、动态列名等场景,但必须严格控制输入来源。

  3. 对比总结

    特性#{}${}
    SQL注入防护
    类型转换自动
    性能高(预编译)略低(字符串拼接)
    使用场景普通参数传递动态结构(表名、列名)


5.2 占位符解析源码分析

在 Mapper XML 的解析过程中,MyBatis 使用 GenericTokenParser 作为通用占位符解析器,同时结合 ParameterMappingTokenHandlerVariableTokenHandler 来完成 ${}#{} 的具体替换。

调用链核心如下:

XMLScriptBuilder.parseScriptNode()
    ↓
MixedSqlNode.apply(DynamicContext context)
    ↓
GenericTokenParser.parse()
    ↓
TokenHandler.handleToken()

#{} 的处理流程

  1. XMLStatementBuilder.parseStatementNode()

    • 获取 SQL 节点内容,进入 LanguageDriver

  2. XMLScriptBuilder

    • 创建 MixedSqlNode,包含多个 SqlNode(动态节点、文本节点等)。

  3. GenericTokenParser

    • 初始化时指定:

      • 起始标记:#{

      • 结束标记:}

      • 处理器:ParameterMappingTokenHandler

  4. ParameterMappingTokenHandler

    • 根据占位符内容(如 iduser.name)创建 ParameterMapping 对象。

    • 返回 ? 作为 SQL 占位符。

源码片段(简化版)

public String handleToken(String content) {
    Object value = context.getBindings().get(content);
    return value == null ? "" : value.toString();
}

${} 的处理流程

  1. 同样由 GenericTokenParser 解析,但使用 VariableTokenHandler

  2. VariableTokenHandler

    • 直接从上下文变量中获取值并替换。

  3. 无参数映射创建,直接返回拼接值。

源码片段(简化版)

public String handleToken(String content) {
    Object value = context.getBindings().get(content);
    return value == null ? "" : value.toString();
}

5.3 安全性与性能优化建议

  1. 安全性建议

    • 避免在 SQL 中直接使用 ${} 处理用户输入。

    • 如果必须使用 ${}(动态表名、列名),应在代码中进行白名单校验:

      List<String> tables = Arrays.asList("user", "order");
      if (!tables.contains(tableName)) {
          throw new IllegalArgumentException("非法表名");
      }
      
    • 对字符串值进行转义,防止拼接引发注入。

  2. 性能优化

    • 尽量使用 #{},利用 JDBC 预编译的缓存能力,减少 SQL 解析与编译开销。

    • 对重复执行的动态 SQL,可以通过缓存 BoundSql 提升效率。

    • 复杂动态 SQL(多层 <if><choose>)应考虑在业务层拼接完毕后交给 MyBatis,减少运行时解析负担。

  3. 调试与排错

    • 启用 MyBatis 日志输出 SQL 及参数绑定信息:

      <settings>
          <setting name="logImpl" value="STDOUT_LOGGING"/>
      </settings>
      
    • 检查最终 SQL 与参数是否匹配。

    • 使用 BoundSql.getSql()BoundSql.getParameterMappings() 进行程序内调试。

  4. 最佳实践总结

    • 数据值用 #{}:安全、性能高。

    • 结构值用 ${}:必须验证来源,防止注入。

    • 混合使用时,先拼结构再绑定数据,避免全拼接。

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 进行优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

探索java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值