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实体 | 说明 |
|---|---|---|---|
< |
标签开始 | < |
Less Than |
> |
标签结束 | > |
Greater Than |
& |
实体引用开始 | & |
Ampersand |
" |
属性值引号 | " |
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 > NOW() <!-- 使用 > 代替 > -->
ORDER BY create_time DESC
LIMIT 1
</select>
<delete id="deleteExpired">
DELETE FROM sms_code
WHERE expire_time < NOW() <!-- 使用 < 代替 < -->
</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 验证修复
-
编译项目:
bashmvn clean compile -
启动应用:
bashcd smart-api mvn spring-boot:run -
确认无异常:
✅ 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<,>) 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 实体转义(
<,>等)
7.3 预防措施
- 开发规范: 团队统一使用 CDATA 包裹 SQL 语句
- 代码审查: CR时重点检查 Mapper XML 文件
- 单元测试: 为每个 Mapper 方法编写单元测试,及早发现问题
- CI/CD: 在 CI 流水线中集成 XML 语法检查工具
- IDE配置: 使用支持 MyBatis 的 IDE 插件(如 MyBatisX),自动提示错误