ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis mapper.xml特殊字符转义与CDATA用法详解

MyBatis mapper.xml特殊字符转义与CDATA用法详解 在 MyBatis 的 mapper.xml 里写 SQL 条件凡是遇到或者!新手基本都栽过一次跟头。明明 SQL 在数据库客户端里跑得好好的一放进 mapper.xml 就飘红报错或者启动时直接抛出The content of elements must consist of well-formed character data or markup又或者org.apache.ibatis.builder.BuilderException。这篇文章我就把这几种写法彻底讲透包括为什么 XML 里不能直接用小于号、什么时候可以不用转义、CDATA 和转义字符怎么选以及我在实际项目里踩过的坑。文章的适用对象很明确正在写 MyBatis 的 Java 后端开发尤其是刚接触 mapper.xml 不久、被各种符号报错折磨过的同学有了一定经验但想系统梳理“test属性里要不要转义”“CDATA 里能不能写标签”这些边界问题的开发者也能从这里找到答案。1. 问题本质为什么 XML 不允许裸写小于号1.1 XML 解析规则决定了是禁区很多人把这个问题当成 MyBatis 的语法其实根源在 XML 本身。mapper.xml 本质是一个 XML 文件任何 XML 解析器在读取它时都要遵循 XML 规范。XML 规范里定义了五个预定义实体其中和是最严格的两个是实体引用的起始符裸写会导致解析器去解析一个不存在的实体。是标签的起始符解析器遇到后会认为后面跟着的是一个标签名如果后面跟的是空格、数字或者 SQL 关键字立刻报格式错误。的情况稍微特殊一点。在 XML 规范中只有出现在字符串]]里才必须转义普通位置的其实可以裸写。所以a 10这段 SQL 放在 XML 里通常不会报错。但行业惯例是统一转义因为你不确定这段 SQL 会不会出现在其他更严格的 XML 解析环境下也为了代码审查时一眼能看出“这里是一个比较操作符”。!就更宽松了因为它里面既没有也没有XML 解析器完全不会拦它。但这里有个隐藏的问题MyBatis 的if标签test属性里写!和 SQL 语句里写!语义并不完全一样文章后面我会专门讲这个坑。1.2 MyBatis 的容错机制与 XML 解析的边界MyBatis 解析 mapper.xml 时底层用的是 XMLParser它会先对整个文件做一次 XML 解析然后再把解析结果交给 SQL 语句构建器。这就意味着只要文件里有任何一个不符合 XML 规范的地方整个 mapper 文件都会加载失败连带着这个 mapper 里的所有 SQL 都不可用。我见过一个真实案例某个项目里一个 mapper.xml 大概有两百多行某次有人在新增 SQL 里写了一个条件WHERE count 10启动时整个应用直接抛BuilderException而且报错信息指的位置是文件开头那几行。排查了很久才发现是中间有一个导致 XML 解析中断解析器报错的位置和真正出错的位置差了很远。这一点大家一定记住XML 解析报错的行号通常不可信真正的凶手可能在整个文件任意一处。2. 四种常见写法与选型指南2.1 方法一XML 转义字符这是最朴素也最兼容的写法在 SQL 里直接用实体引用替代特殊字符原符号转义写法lt;gt;lt;gt;amp;单引号apos;双引号quot;举个例子查询年龄在某个区间内的用户select idselectByAgeRange resultTypeUser SELECT * FROM user WHERE age gt; #{minAge} AND age lt; #{maxAge} /select这种写法最稳妥XML 解析器一眼就能看明白而且和 CDATA 相比它不会影响 MyBatis 动态标签的解析。但因为lt;和gt;可读性确实比较差SQL 稍微一长就眼花。我的经验是单个条件、SQL 片段较短时用转义条件多、SQL 复杂时优先考虑 CDATA。2.2 方法二CDATA 区CDATA 是 XML 里用来包裹“不需要解析的文本”的标记写法是![CDATA[ 你的内容 ]]。放在 CDATA 区里的内容XML 解析器会原封不动地当成纯文本不解析任何标签和实体。所以里面可以放心裸写、、、、!select idselectByAgeRange resultTypeUser SELECT * FROM user WHERE age ![CDATA[ ]] #{minAge} AND age ![CDATA[ ]] #{maxAge} /select这是我最推荐的写法。但注意一个细节我在例子里只把操作符包进了 CDATA而不是把整条 SQL 包进去。为什么不把整条 SQL 都包起来因为一旦把整条 SQL 放进 CDATAMyBatis 的动态标签if、where、foreach等就会全部失效。CDATA 的内容是“原样文本”MyBatis 的 SQL 节点构建器不会去解析 CDATA 内部的 MyBatis 标签如果标签写在 CDATA 里它们会被当作普通字符串拼进 SQL最终执行时报数据库语法错误。有人会问那我只用 CDATA 包住特殊符号前后的部分会不会把 SQL 截断不会。CDATA 只是标记这一段文本不参与 XML 解析拼接后的最终 SQL 是完整连续的。比如age ![CDATA[ ]] #{minAge}解析后的 SQL 就是age #{minAge}。2.3 方法三借助![CDATA[ ]]包裹整个比较条件如果你觉得上面的写法把 CDATA 拆得太碎可以把整个条件表达式包进去select idselectByAgeRange resultTypeUser SELECT * FROM user ![CDATA[ WHERE age #{minAge} AND age #{maxAge} ]] /select注意这里没写where标签因为整个条件都在 CDATA 内部where标签如果放在 CDATA 外部它无法识别 CDATA 内部的条件要不要加WHERE关键字。这种写法的最大问题是一旦条件变成动态的比如某些情况下 minAge 不传这个方案就废了。所以它只适合条件完全固定的场景。2.4 方法四test属性里的特殊符号这里必须区分两个完全不同的场景一个是 SQL 语句里的比较符一个是 MyBatis 动态 SQL 标签中test属性的比较表达式。select idselectUsers resultTypeUser SELECT * FROM user where if testminAge ! null and minAge 0 AND age gt; #{minAge} /if /where /select重点来了test属性里写、、!都是合法的不需要转义。原因在于test属性值本身处于引号包裹的属性取值阶段XML 解析器读取到引号内的内容后不会继续把它当作 XML 标记来解析。MyBatis 拿到test表达式的字符串后会用 OGNL 表达式引擎解析而 OGNL 是支持、!这些标准运算符的。但是有个特例test属性里的仍然建议转义因为在属性值里出现部分 XML 解析器在属性值规范化时也会出问题。稳妥起见test里我统一写成lt;或者直接用的等价形式不赌解析器的容错度。3. 实操案例一个完整的分页范围查询下面给出一个实际可用的 mapper.xml 片段覆盖常见的“时间范围 状态过滤 金额大于/小于”组合查询。假设有一张订单表orders字段包括order_id、user_id、amount、pay_time、status。select idselectOrdersByCondition resultTypeOrder SELECT order_id, user_id, amount, pay_time, status FROM orders where if testuserId ! null and userId 0 AND user_id #{userId} /if if testminAmount ! null and minAmount 0 AND amount ![CDATA[ ]] #{minAmount} /if if testmaxAmount ! null and maxAmount 0 AND amount ![CDATA[ ]] #{maxAmount} /if if teststartTime ! null AND pay_time ![CDATA[ ]] #{startTime} /if if testendTime ! null AND pay_time ![CDATA[ ]] #{endTime} /if if teststatus ! null and status ! AND status #{status} /if /where ORDER BY pay_time DESC /select这段 XML 里的几个典型写法我在实际项目中这样用的理由如下test属性里直接用和因为它们在属性值里完全安全不需要转义写起来也清爽。SQL 语句里的比较操作符全用 CDATA 包裹保证 XML 解析和 MyBatis 动态标签互不干扰。status ! null and status ! 这个判断专门处理字符串既过滤掉 null又过滤掉空字符串。where标签会自动处理第一个条件前面的AND所以 MySQL 里不会出现WHERE AND amount ...这种语法错误。3.1 不同写法的混用原则在实际项目中一个 mapper.xml 里往往同时存在多种写法这很正常。我的建议是定一个团队内统一的原则避免每个人按自己习惯乱写。我个人用的原则是test属性里的比较表达式、、、!直接写不转义写为lt;。SQL 片段里的比较操作符用 CDATA 包住单个操作符如![CDATA[ ]]、![CDATA[ ]]。固定条件的 SQL 片段允许用转义字符但保持统一风格。CDATA 内部绝对不写 MyBatis 动态标签避免标签失效。这个原则不一定适合所有团队但一定要有。因为代码的可维护性有时候不在于唯一的正确方案而在于“大家写的都一样看到哪个文件都不陌生”。3.2 小技巧用注解或测试用例验证 SQL很多人写完 mapper.xml 靠启动时不报错来判断 SQL 写得对不对这个标准太低了。XML 合法只能说明文件结构没毛病SQL 语法对不对只有真正执行了才知道。我的习惯是给这类复杂查询写一个单元测试用 H2 内存数据库配合 MyBatis 直接跑 SQL把 mapper 的输出 SQL 打到日志里人工确认一遍。SpringBootTest public class OrderMapperTest { Autowired private OrderMapper orderMapper; Test public void testSelectOrdersByCondition() { OrderQuery query new OrderQuery(); query.setMinAmount(100.0); query.setMaxAmount(500.0); query.setStartTime(LocalDateTime.of(2024, 1, 1, 0, 0)); ListOrder orders orderMapper.selectOrdersByCondition(query); System.out.println(orders.size()); } }启动测试时把 MyBatis 的 SQL 日志级别调成 DEBUG控制台会打印出完整的 SQL 和参数。对着日志看一秒钟 SQL比盲猜半天强得多。4. 常见报错与排查技巧实录4.1 报错一The content of elements must consist of well-formed character data or markup这是最常见的 XML 格式错误触发原因就是 SQL 里裸写了小于号。比如select idselectByAge resultTypeUser SELECT * FROM user WHERE age 18 /select解析器看到 18的时候会把当标签起始符后面的空格直接导致解析失败。处理方式就是转义lt;或者用 CDATA 包住。4.2 报错二CDATA 里写了if导致标签不生效有人图省事把整段 SQL 连同if标签一起写进 CDATA结果发现条件过滤完全失效。原因我在前面说过CDATA 里的内容被当成纯文本MyBatis 不会解析其中的动态标签。判断方法很简单看日志里输出的 SQL 是否原样包含了if标签字符串。如果含了说明标签写进了 CDATA。4.3 报错三test属性里用匹配数值时结果诡异比如if testminAge 5 AND age gt; #{minAge} /if这个写法本身合法OGNL 会把minAge 5解析成数值比较。但如果minAge是从前端传上来的字符串类型这里比较的其实是字符串的大小结果可能和你预期的完全不一样。所以test属性里的数值比较建议先做类型转换和 null 判断if testminAge ! null and minAge.toString().length() 0 AND age gt; #{minAge} /if或者更稳妥一点在 DTO 层就用 Integer/Long 接收避免 OGNL 处理字符串和数值混合比较的边界问题。4.4 报错四启动时这个 mapper 正常跑批时 SQL 才报错这种情况多见于${}拼接的场景。如果动态 SQL 用了${}而不是#{}字符串替换发生在 SQL 编译之前比如select idselectByTable resultTypemap SELECT * FROM ${tableName} WHERE amount ![CDATA[ ]] #{minAmount} /select如果tableName被替换为orders WHERE statusACTIVE之类的字符串最终 SQL 就变成SELECT * FROM orders WHERE statusACTIVE WHERE amount ?直接数据库语法错误。这种问题 XML 层是发现不了的排查时优先打印 MyBatis 执行前的完整 SQL 日志一眼就能看出拼接问题。4.5 实战心得统一处理 Symbol 的编码我后来在团队里定了一个规矩所有 mapper.xml 文件头加一行注释明确写清楚“小于号统一用 CDATA小于号只出现在 test 属性时才允许转义写法”。这样新来的同事看代码时至少有个明确指引不至于每个文件一个风格。另外一个私藏技巧用 IDE 的实时 XML 校验。IntelliJ IDEA 默认会对 XML 文件做格式校验只要在 XML 里裸写了编辑器会立刻标红。建议所有写 mapper.xml 的同事都开着这个校验不要关闭。它能帮你把问题拦截在编码阶段。5. 写在最后的一点经验如果你还在纠结到底哪种写法最标准我的建议是根据场景选而不是根据“最标准”选。判断标准很简单——这段 SQL 是不是属于动态 SQL 的一部分。如果是用 CDATA 包操作符如果不是用转义字符就够了。核心思路是避免 XML 解析器和 MyBatis 动态标签解析两层机制互相踩脚。记住一个最关键的认知mapper.xml 是 XML 文件不是纯 SQL 文本文件。你写的每一段 SQL在真正到达数据库之前都要先过 XML 解析这一关。把“这个符号在 XML 里怎么表示”当成第一反应而不是“为什么 SQL 客户端能跑这里却报错”后面所有坑都能绕开。这篇文章的内容都是我实际项目中踩过来、填平之后的记录照着上面的方案写不敢说一定最优但至少能让你少折腾半天。
返回列表