MyBatis XML Mapper 特殊字符处理方案

1. 问题描述

1.1 错误现象

后端服务启动时抛出 MyBatis XML Mapper 解析异常,导致应用无法正常启动。

1.2 错误堆栈

复制代码
Caused by: org.apache.ibatis.builder.BuilderException: Error creating document instance.
  Cause: org.xml.sax.SAXParseException; lineNumber: 34; columnNumber: 28;
  元素内容必须由格式正确的字符数据或标记组成。
    at org.apache.ibatis.parsing.XPathParser.createDocument(XPathParser.java:262)
    at org.apache.ibatis.parsing.XPathParser.<init>(XPathParser.java:127)
    at com.baomidou.mybatisplus.core.MybatisXMLMapperBuilder.<init>(MybatisXMLMapperBuilder.java:87)
    at com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean.buildSqlSessionFactory(MybatisSqlSessionFactoryBean.java:681)
    ... 105 common frames omitted
Caused by: org.xml.sax.SAXParseException: 元素内容必须由格式正确的字符数据或标记组成。

1.3 错误位置

  • 文件 : smart-auth/src/main/resources/mapper/SmsCodeMapper.xml
  • 行号: 第 34 行,第 28 列
  • 错误语句 : WHERE expire_time < NOW()

2. 问题根因分析

2.1 XML 特殊字符冲突

在 XML 文档中,以下字符具有特殊含义,不能直接在元素内容中使用:

字符 含义 XML实体 说明
< 标签开始 &lt; Less Than
> 标签结束 &gt; Greater Than
& 实体引用开始 &amp; Ampersand
" 属性值引号 &quot; Quotation
' 属性值引号 ' Apostrophe

2.2 问题代码示例

错误写法 ❌:

xml 复制代码
<select id="selectLatestUnusedByMobile" resultMap="BaseResultMap">
    SELECT * FROM sms_code
    WHERE mobile = #{mobile}
    AND used_status = 0
    AND expire_time > NOW()    <!-- XML解析器会将 > 视为标签结束符 -->
    ORDER BY create_time DESC
    LIMIT 1
</select>

<delete id="deleteExpired">
    DELETE FROM sms_code
    WHERE expire_time < NOW()   <!-- XML解析器会将 < 视为标签开始符 -->
</delete>

2.3 问题原因

XML 解析器在解析 Mapper 文件时,遇到 < 或 > 符号会误认为是 XML 标签的一部分,导致解析失败:

  • expire_time > NOW() → 解析器认为 > 是标签结束符
  • expire_time < NOW() → 解析器认为 < 是新标签的开始
  • 实际这些符号是 SQL 比较运算符,应作为普通文本处理

3. 解决方案

3.1 方案一:使用 XML 实体转义(不推荐)

将特殊字符替换为对应的 XML 实体:

xml 复制代码
<select id="selectLatestUnusedByMobile" resultMap="BaseResultMap">
    SELECT * FROM sms_code
    WHERE mobile = #{mobile}
    AND used_status = 0
    AND expire_time &gt; NOW()    <!-- 使用 &gt; 代替 > -->
    ORDER BY create_time DESC
    LIMIT 1
</select>

<delete id="deleteExpired">
    DELETE FROM sms_code
    WHERE expire_time &lt; NOW()  <!-- 使用 &lt; 代替 < -->
</delete>

优点:

  • 符合 XML 规范
  • 无需额外语法

缺点:

  • 可读性差
  • 容易遗漏
  • 维护成本高

3.2 方案二:使用 CDATA 区段(推荐)✅

使用 <![CDATA[]]> 包裹 SQL 语句,CDATA 内的内容不会被 XML 解析器处理:

xml 复制代码
<select id="selectLatestUnusedByMobile" resultMap="BaseResultMap">
    <![CDATA[
    SELECT * FROM sms_code
    WHERE mobile = #{mobile}
    AND used_status = 0
    AND expire_time > NOW()    <!-- 可以直接使用 > 符号 -->
    ORDER BY create_time DESC
    LIMIT 1
    ]]>
</select>

<delete id="deleteExpired">
    <![CDATA[
    DELETE FROM sms_code
    WHERE expire_time < NOW()  <!-- 可以直接使用 < 符号 -->
    ]]>
</delete>

优点:

  • ✅ 可读性强,SQL 语句保持原样
  • ✅ 无需转义,减少出错概率
  • ✅ MyBatis 官方推荐写法
  • ✅ 支持所有特殊字符(<, >, &, ", ')
  • ✅ 便于维护和调试

缺点:

  • 需要额外包裹语法(可忽略)

4. 实施步骤

4.1 修复 SmsCodeMapper.xml

文件路径 : smart-auth/src/main/resources/mapper/SmsCodeMapper.xml

修改前:

xml 复制代码
<select id="selectLatestUnusedByMobile" resultMap="BaseResultMap">
    SELECT * FROM sms_code
    WHERE mobile = #{mobile}
    AND used_status = 0
    AND expire_time > NOW()
    ORDER BY create_time DESC
    LIMIT 1
</select>

<delete id="deleteExpired">
    DELETE FROM sms_code
    WHERE expire_time < NOW()
</delete>

修改后:

xml 复制代码
<select id="selectLatestUnusedByMobile" resultMap="BaseResultMap">
    <![CDATA[
    SELECT * FROM sms_code
    WHERE mobile = #{mobile}
    AND used_status = 0
    AND expire_time > NOW()
    ORDER BY create_time DESC
    LIMIT 1
    ]]>
</select>

<delete id="deleteExpired">
    <![CDATA[
    DELETE FROM sms_code
    WHERE expire_time < NOW()
    ]]>
</delete>

4.2 验证修复

  1. 编译项目:

    bash 复制代码
    mvn clean compile
  2. 启动应用:

    bash 复制代码
    cd smart-api
    mvn spring-boot:run
  3. 确认无异常:

    复制代码
    ✅ Application started successfully
    ✅ No XML parsing errors

5. 最佳实践与规范

5.1 MyBatis XML Mapper 编码规范

5.1.1 SQL 语句统一使用 CDATA

强制要求 :所有包含特殊字符的 SQL 语句必须使用 <![CDATA[]]> 包裹

xml 复制代码
<!-- 正确示例 ✅ -->
<select id="queryUsers" resultType="User">
    <![CDATA[
    SELECT * FROM users
    WHERE age > 18
    AND status <> 'DELETED'
    AND name LIKE CONCAT('%', #{keyword}, '%')
    ]]>
</select>

<!-- 错误示例 ❌ -->
<select id="queryUsers" resultType="User">
    SELECT * FROM users
    WHERE age > 18           <!-- XML 解析错误 -->
    AND status <> 'DELETED'  <!-- XML 解析错误 -->
</select>
5.1.2 特殊字符清单

需要使用 CDATA 或转义的场景:

SQL 语句 特殊字符 原因
age > 18 > 标签结束符
age < 18 < 标签开始符
status <> 'DELETED' <> 不等于运算符
value >= 100 >= 大于等于
value <= 100 <= 小于等于
name LIKE 'A&B' & 实体引用开始
动态拼接多个条件 多个特殊字符 复杂SQL

5.2 CDATA 使用注意事项

✅ 推荐做法
xml 复制代码
<!-- 1. 整体包裹 SQL -->
<select id="query" resultMap="BaseResultMap">
    <![CDATA[
    SELECT * FROM table
    WHERE condition > 0
    ]]>
</select>

<!-- 2. 保持缩进美观 -->
<select id="query" resultMap="BaseResultMap">
    <![CDATA[
        SELECT
            id, name, age
        FROM users
        WHERE age > 18
        AND status <> 'DELETED'
        ORDER BY create_time DESC
    ]]>
</select>

<!-- 3. 复杂SQL分段处理 -->
<select id="complexQuery" resultMap="BaseResultMap">
    SELECT * FROM users
    <where>
        <if test="age != null">
            <![CDATA[ AND age >= #{age} ]]>
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>
❌ 错误做法
xml 复制代码
<!-- 1. 嵌套CDATA(不支持) -->
<select id="query">
    <![CDATA[
        SELECT * FROM (
            <![CDATA[ SELECT id FROM temp ]]>  <!-- ❌ CDATA不能嵌套 -->
        ) t
    ]]>
</select>

<!-- 2. CDATA内使用动态SQL标签(无效) -->
<select id="query">
    <![CDATA[
        SELECT * FROM users
        <if test="age != null">  <!-- ❌ 动态标签在CDATA内不生效 -->
            AND age > #{age}
        </if>
    ]]>
</select>

5.3 代码审查检查清单

在代码审查(Code Review)时,检查以下内容:

  • 所有 Mapper XML 文件中的比较运算符(<, >, <=, >=, <>)是否使用了 CDATA 或正确转义
  • CDATA 区段是否正确闭合
  • 动态 SQL 标签是否在 CDATA 外部
  • SQL 缩进是否规范、可读性是否良好
  • 是否有不必要的 CDATA(无特殊字符的SQL不需要CDATA)

6. 相关知识扩展

6.1 XML 规范(W3C标准)

根据 XML 1.0 规范:

2.4 Character Data and Markup

Text consists of intermingled character data and markup. Markup takes the form of start-tags, end-tags, empty-element tags, entity references, character references, comments, CDATA section delimiters, document type declarations, processing instructions, XML declarations, text declarations, and any white space that is at the top level of the document entity.

6.2 MyBatis 官方文档

MyBatis 官方文档对 CDATA 的说明:

Mapper XML Files - SQL

If you need to use special characters like <, > in your SQL, you should either escape them (using &lt;, &gt;) or wrap the SQL in a CDATA section.

参考链接:

6.3 常见框架对比

框架 XML特殊字符处理 推荐方案
MyBatis 支持CDATA、实体转义 CDATA
Hibernate HQL中无此问题 -
Spring JDBC Java代码中无此问题 -
JPA/JPQL JPQL中无此问题 -

7. 总结

7.1 问题回顾

  • 问题 : MyBatis XML Mapper 中使用 <, > 等 SQL 比较运算符导致 XML 解析失败
  • 原因: XML 将这些符号视为标签定界符,而非文本内容
  • 影响: 应用无法启动,阻塞开发和部署

7.2 解决方案

  • ✅ 推荐方案 : 使用 <![CDATA[]]> 包裹 SQL 语句
  • ⚠️ 备选方案 : 使用 XML 实体转义(&lt;, &gt;等)

7.3 预防措施

  1. 开发规范: 团队统一使用 CDATA 包裹 SQL 语句
  2. 代码审查: CR时重点检查 Mapper XML 文件
  3. 单元测试: 为每个 Mapper 方法编写单元测试,及早发现问题
  4. CI/CD: 在 CI 流水线中集成 XML 语法检查工具
  5. IDE配置: 使用支持 MyBatis 的 IDE 插件(如 MyBatisX),自动提示错误
相关推荐
西凉的悲伤33 分钟前
MyBatis 中 association 与 collection 详解
java·mybatis
天空鸟_时光不老2 小时前
10-Agent安全隐患与SQL执行层加固
java·数据库·人工智能·sql·spring·maven·mybatis
jianjinwssy1 天前
MyBatis-Plus 从入门到实战:常用功能与核心原理
java·mybatis
ss2731 天前
AI全栈实战 | 3.6-01 数据库与 ORM:学过 MyBatis 再看 SQLAlchemy,才知道“半自动“和“全自动“差在哪
数据库·mybatis
hasty3 天前
policy.xml 在,不代表策略生效:ImageMagick 补丁给安全配置验收的提醒
xml·mysql·安全
代码什么用5 天前
使用javassist生成类 && MyBatis中接口代理机制及使用
mybatis
paopaokaka_luck5 天前
非遗文物数字化小程序(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
javascript·spring boot·mysql·数据分析·echarts·mybatis
韩曙亮5 天前
【JavaEE】MyBatis 基本用法 ⑤ ( 单条件查询 | 多条件查询 | 多条件动态查询 | 单条件动态查询 )
java·sql·java-ee·mybatis·mapper·sql映射
x-Achan5 天前
Redis实现方案:更新策略、穿透、雪崩、击穿、工具封装
java·spring boot·redis·spring·bootstrap·mybatis
泡海椒6 天前
JQuick-Excel JQuickRow 数据准备实战:JObjectConverter、JQuickRow.toRows 与 XML 导出链路
xml·windows·excel