尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MyBatis XML特殊字符转义与CDATA实战:彻底解决小于号崩溃问题

MyBatis XML特殊字符转义与CDATA实战:彻底解决小于号崩溃问题 1. 为什么 MyBatis 的 XML 里一个小于号能让你整段 SQL 崩掉先说一个我最早接手 MyBatis 项目时遇到的真实场景项目里有一条订单查询要查创建时间早于某个点的记录开发同学直接在 mapper XML 里写了这么一句WHERE create_time #{endTime}结果应用一启动控制台立刻抛了一坨异常最显眼的是The content of elements must consist of well-formed character data or markup.很多新手看到这个报错直接懵了明明 SQL 在数据库客户端里跑得好好的怎么一放到 MyBatis 里就崩还有人以为是 SQL 语法错误反复去调字段名、表名却始终找不到问题。我之所以把这篇文章的第一节放在解析器上就是想先把根因讲清楚MyBatis 的 mapper 文件首先是一个 XML 文档其次才是 SQL 映射配置。1.1 XML 解析器先于 SQL 执行特殊字符问题的优先级MyBatis 启动时会通过 XML 解析器去读取所有的 mapper 映射文件把每个select、insert、update、delete节点和内部 SQL 片段解析成内部配置对象。这个阶段发生在任何 SQL 执行之前。也就是说不管你 SQL 写得多漂亮如果 XML 本身不符合规范MyBatis 根本走不到执行那一步。在 XML 规范里是标签的开始标记解析器看到就会认为后面跟着的是一个标签名。你写create_time #{endTime}解析器会尝试把 #{endTime}当标签处理然而#和空格都不符合标签命名规则于是解析器直接判定该文档不是 well-formed格式良好启动阶段就报错。在普通文本内容里其实是可以出现的但如果你在属性值里直接用或者某些解析器也会因为历史兼容性问题给出警告甚至报错。最保险的做法是只要在 XML 里用到比较操作符一律按特殊字符处理。1.2 XML 五个预定义实体与更多隐患XML 规范定义了五个必须在源码中写成实体形式的字符MyBatis 开发里最常用到的就是前三个原始字符实体写法常见出现场景lt;小于比较、动态标签条件gt;大于比较、之类amp;参数里带 URL、SQL 函数拼接apos;字符串常量包含单引号quot;属性值里包含双引号我见过有人把lt;拼错成lgt或者问小于号在 XML 里是不是 lgt其实lt就是less than的缩写gt是greater than的缩写记住这个命名规则就不容易写错。还有一个容易忽略的字符是比如你在 SQL 里做字符串函数拼接SELECT * FROM t_user WHERE instr(username, #{kw}) 0看起来没问题但如果你直接在 XML 里写WHERE flag A flag2 B这显然不合法只是举例。真正常见的是动态列名或某些数据库函数里用到比如 Oracle 的TO_CHAR、DECODE拼接或者部分 MySQL 函数。在 XML 里必须写成amp;否则解析器会认为它是一个实体引用的开始后面跟不到分号就报错。我的一贯建议是不要跟 XML 解析器玩猜谜凡是出现需要转义的原始字符能写成实体的就写实体能包 CDATA 的就包 CDATA后面我会专门讲这两条路线的选择。2. 转义与 CDATA两条最常用的处理路线但别用错场合处理 MyBatis XML 特殊字符业界基本就两招实体转义和 CDATA 包裹。这两招都能让解析器闭嘴但使用场合和代价完全不同。2.1 什么时候用实体转义什么时候用 CDATA实体转义就是把写成lt;写成gt;写成gt;写成lt;。优点是简单直接写起来可控尤其在if test...、when test...这些动态标签的属性值里实体转义几乎是唯一干净的选择。缺点也很明显SQL 可读性直线下降一长串时间比较条件写下来满屏lt;gt;后期维护的人要花额外精力来翻译。CDATA 的写法是![CDATA[ SQL 片段 ]]CDATA 是 Character Data 的缩写它的意思是告诉 XML 解析器这里面的内容全部当成纯文本不要尝试解析任何标签、实体、注释。这样你在里面写、、都合法。MyBatis 里最经典的用法就是用 CDATA 把一条带比较运算符的完整 SQL 片段包起来select idselectByTime resultTypecom.example.Order SELECT * FROM t_order where if testendTime ! null ![CDATA[ AND create_time #{endTime} ]] /if /where /select注意CDATA 不是 SQL 注释它的内容最终仍然会被 MyBatis 当作 SQL 主体来处理。它只是绕过了 XML 解析器。另外CDATA 内部不能出现字符串]]因为这是 CDATA 的结束标记。如果 SQL 里真的需要这个字面值几乎不会遇到那就只能拆分成多个 CDATA 片段拼接或改用实体转义。2.2 一个常见误区CDATA 里放#{}会不会失效不少人刚接触 CDATA 时会担心我把 SQL 包进 CDATA里面写的#{param}占位符还有没有用这里我可以明确告诉你完全有用。CDATA 影响的是 XML 解析层的动作MyBatis 在完成 XML 解析之后会继续处理 SQL 文本里的#{}、${}等占位符。两者完全不在一个阶段不会互相干扰。还有另一个误区是有人把整个select节点内容全部用 CDATA 包起来包括where、if这些动态标签比如select idbad resultType... ![CDATA[ where if testid ! null AND id #{id} /if /where ]] /select这样是绝对不行的。CDATA 一旦包住where、if这些标签就变成了纯文本MyBatis 不会再去解析它们。最后你得到的 SQL 可能是一串包含where字样的垃圾文本或者直接执行报错。正确的做法是用if、where等动态标签做条件控制只在含有特殊字符的 SQL 片段外面套 CDATA两者是包含关系不是对等关系。我个人的习惯是单个比较符号、写在标签属性里的判断使用实体转义一段连续 SQL 里有多个比较运算符比如时间范围、金额范围优先使用 CDATA并且只包住必要的片段保证动态标签仍然在外层正常工作。3. 动态 SQL 里最容易踩的三个坑标签内判定、写法、模糊查询通配符如果说静态 SQL 里的特殊字符问题还好解决那动态 SQL 里的坑就隐蔽多了。因为你会遇到两个解析器的碰撞外部 XML 解析器和 MyBatis 内部动态标签解析器。很多问题出现在两层解析的交叉地带。3.1if、where标签中的大小于判定先看这段代码select idselectOrders resultTypeOrder SELECT * FROM t_order where if teststartDate ! null and startDate endDate create_date BETWEEN #{startDate} AND #{endDate} /if /where /select第一眼看上去逻辑没问题但启动时会报错。问题出在if test...属性值里直接写了。XML 属性值是双引号包裹的普通文本解析器遇到同样会认为开始一个新标签。所以哪怕你只是在一个判断条件里用了小于号也必须写成if teststartDate ! null and startDate lt; endDate或者干脆反转比较逻辑写成endDate startDate这样属性值里没有就不需要转义。这里我建议优先用反转写法因为lt;在属性值里可读性真的不算好而且容易看漏。但反转写法也有局限如果比较语义明显比如开始时间不能超过结束时间反转成结束时间大于等于开始时间完全是等价的没有任何副作用可以放心用。3.2与出现在 SQL 主体里的处理策略如果比较条件出现在 SQL 主体而不是标签属性里比如if testminPrice ! null AND price #{minPrice} /if if testmaxPrice ! null AND price #{maxPrice} /if和都出现在文本节点里。虽然在 XML 文本节点中通常允许直接出现但里的是必须处理的。我建议即使只是也统一转义或者用 CDATA 包裹。看看下面这种写法if testminPrice ! null ![CDATA[ AND price #{minPrice} ]] /if if testmaxPrice ! null ![CDATA[ AND price #{maxPrice} ]] /if这是一个典型的正确姿势动态标签在外面控制 SQL 拼接CDATA 只包住包含比较运算符的 SQL 片段。不是说每个比较都要这样写而是当某个条件里同时出现和混用或者有多个比较符号时CDATA 比逐个转义清爽得多。3.3 模糊查询里%和_的隐藏坑特殊字符不光指、、这类 XML 字符还有 SQL 模式匹配里的通配符。比如你要按关键字模糊查询用户很多初学者习惯直接在 XML 里拼AND username LIKE %${keyword}%先说${}的问题它做的是字符串替换不是预编译用户传入%或_会改变 LIKE 语义更危险的是可能注入恶意 SQL。正确的做法是用#{}配合数据库拼接函数AND username LIKE CONCAT(%, #{keyword}, %)这样%和_在参数里会作为普通字符处理不会影响模式匹配。假设业务上确实希望支持%和_作为通配符使用那就需要在 Java 层对用户输入做白名单或清洗而不是在 SQL 层冒险。另外如果你用的是 MySQLLIKE语法里可能会需要ESCAPE子句比如AND username LIKE CONCAT(%, #{keyword}, %) ESCAPE /这个/在 XML 里没有任何特殊含义不需要转义但它能帮你把参数中的/、%、_转义成字面值。这部分属于 SQL 语义层面的边界问题不在 XML 解析层但实际排查特殊字符问题时经常一起暴露所以放在这一节提醒。4. 一次真实排错时间范围查询报错日志只有一半问题出在哪儿这一节我带大家完整复盘一次线上问题的排查过程。这个案例融合了特殊字符、启动报错、SQL 日志残缺三个典型现象可以说把 MyBatis XML 的坑踩了一个遍。4.1 现象与第一反应以为是数据库连接问题某天测试环境报了一个接口 500查看后台日志最显眼的异常是org.apache.ibatis.builder.BuilderException: Error creating document instance. Caused by: org.xml.sax.SAXParseException: The content of elements must consist of well-formed character data or markup.很多人的第一反应是SQL 写错了或数据库连不上了实际上BuilderException和SAXParseException两个关键词已经把范围缩得很小MyBatis 在构建 SQL 映射时解析 XML 文档失败。这跟数据库本身没有关系甚至跟 SQL 语义都没有关系纯粹是 XML 格式不合法。4.2 完整排查链路从报错行号到特殊字符拿到异常后我先把异常堆栈往前翻找到类似org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:...)它一般会指明是哪个 mapper 文件解析失败甚至精确到行号。我的经验是先看行号再打开对应 XML 文件把光标移到那一行。通常你会发现要么是直接出现在 SQL 文本里要么是后面跟了个空格。当时我们看到的 mapper 是订单查询原片段长这样select idselectByCreateTime resultTypemap SELECT * FROM t_order WHERE create_time ![CDATA[ #{endTime} ]] /select不对这个例子是后修的。实际出问题的代码是这样的select idselectByCreateTime resultTypemap SELECT * FROM t_order WHERE create_time #{endTime} /select启动阶段直接报错。这里我建议排查时做一个二分操作先把可能引发问题的条件逐个注释掉每注释一次启动一次能很快锁定是哪个if分支里的哪一段 SQL 出了问题。尤其是当 XML 文件几百行、报错行号又不精确的时候二分排除比肉眼硬看高效得多。4.3 修复方案与验证转义和 CDATA 都可以定位到是create_time #{endTime}这一行后修复方案有两个我把两种都列出来方案一实体转义select idselectByCreateTime resultTypemap SELECT * FROM t_order WHERE create_time lt; #{endTime} /select方案二CDATAselect idselectByCreateTime resultTypemap SELECT * FROM t_order WHERE create_time ![CDATA[ #{endTime} ]] /select两个方案执行效果完全一样。考虑到这条 SQL 后续还会加更多时间比较条件我最终用了 CDATA因为里面即使再出现、、也不需要逐个转义。验证方式也很简单重启应用查看日志中打印出的 SQL 是否完整然后调用接口传入时间参数确认查询结果符合预期。如果项目里配了 MyBatis SQL 日志插件你会看到预处理后的 SQL 和绑定参数值此时#{}会被替换成?说明解析已经正常完成特殊字符没有影响预编译。我还想提醒一个容易被忽略的点项目里如果使用了 MyBatis 的二级缓存修改 XML 后必须清缓存或重启应用否则可能出现改了映射文件但实际执行还是旧 SQL 的诡异现象。这一点在很多团队里都踩过尤其是在测试环境热部署不彻底的时候。5. 再深一层#{}、${}与特殊字符的纠缠以及字符集对乱码的影响特殊字符的处理不能只停留在 XML 解析层还要看数据是怎么进入 SQL 的。MyBatis 的参数占位符有两种它们和特殊字符的关系完全不同。5.1 预编译占位符与字符串替换的天壤之别#{}是预编译占位符。MyBatis 会在运行时把 SQL 中的#{}替换成?然后通过 JDBC 的PreparedStatement绑定参数。这意味着即使参数里包含、、、这样的字符它们也只是被当成普通字符串值既不会影响 XML 解析也不会破坏 SQL 结构。所以理论上只要你在写 SQL 时正确转义了 XML 特殊字符执行层面的特殊字符压力很小。${}则完全不同它做的是直接字符串拼接。比如AND status ${status}如果status的值为CLOSED拼接后 SQL 就是AND status CLOSED。但假如外部传入1 OR 11你得到的 SQL 就是AND status 1 OR 11直接变成一个恒真条件。更麻烦的是有些人为了规避 XML 报错会把写成lt;后发现#{}在某些场景下不好用就换${}硬拼这属于用更大的漏洞去补一个小坑绝对不建议。还有一个使用细节在ORDER BY、动态表名、动态列名这些位置#{}不会生效因为预编译占位符不能出现在表名、列名位置。此时必须用${}但一定要做白名单校验比如用 Map 映射或枚举限制传入值范围不能直接信任外部参数。5.2 字符集编码特殊字符变成乱码的另一个来源特殊字符解析成功之后还要保证编码一致性。很多 XML 文件顶部会写?xml version1.0 encodingUTF-8?但 IDE 或编辑器保存文件时用的可能是 GBK 或 ISO-8859-1两者不一致会导致中文和全角符号变成乱码。比如 SQL 里写了一个中文全角大于号在 XML 解析时不一定会报错但到了数据库执行阶段可能变成?或乱码最终查不出数据。我处理过一个类似问题SQL 条件里有一个实际上是全角字符应用从 XML 读取后变成乱码导致比对永远失败。这个问题的排查思路和特殊字符不太一样得检查文件编码、数据库连接 URL 的characterEncoding配置、以及数据库表本身的字符集。这里提醒一句XML 声明 encoding 要与文件实际保存编码一致数据库连接的编码也要与数据库会话编码一致三层贯穿任何一层断了特殊字符和中文都会出问题。5.3 带有 HTML 实体风格的场景nbsp;在 XML 里不可直接用还有一种特殊字符容易被忽略就是类似nbsp;、copy;这样的 HTML 实体。HTML 里它们很常见但 XML 标准只预定义了五个实体nbsp;并没有在 XML 预定义实体的名单里。如果数据里包含nbsp;在 XML 中直接写会被解析器视为非法实体引用需要写成#160;或直接写原始字符的 UTF-8 编码。MyBatis 映射文件里不常遇到这种场景但当你处理富文本内容或从第三方接口拿到包含 HTML 实体的数据再拼进 SQL 时就要格外小心。6. 一套可以直接复用的特殊字符检查清单与自测技巧到这里原理和案例都讲得差不多了。最后我把多年积累的检查清单和自测方法整理出来方便你直接抄作业。6.1 检查清单写完 mapper XML 后逐项过一遍文本节点中出现时是否用了lt;或 CDATA标签属性值test、value、key等中出现时是否用了lt;连续比较条件中是否混入了、有没有统一处理SQL 中是否包含比如 URL 参数、函数拼接是否写成amp;是否误把 HTML 实体如nbsp;写进了 XML使用了 CDATA 时CDATA 内部是否误放了if、where等动态标签是否在不需要${}的地方用了${}导致外部输入直接进入 SQLXML 声明的 encoding 与实际保存编码是否一致应用启动后是否确认日志中打印的 SQL 与预期一致可以打印成一张小卡片贴在工位旁边每次写完 mapper 文件就对照过一遍。别看这些条目琐碎我见过不少生产事故最后都能归到其中一条。6.2 两个自测技巧把报错留在开发期第一个技巧是写一个极简的单元测试在项目启动阶段就去解析所有 mapper 文件。比如用 Spring Boot 的话其实项目启动时 MyBatis 就会加载所有 XML只要测试类里启动一次 Spring 容器任何 XML 格式问题都会在这里暴露。如果你的项目是裸 MyBatis没有 Spring 容器也可以直接构建SqlSessionFactoryReader reader Resources.getResourceAsReader(mybatis-config.xml); SqlSessionFactory factory new SqlSessionFactoryBuilder().build(reader);只要这一步能成功说明至少 XML 结构是合法的。第二个技巧是用 IDE 自带的 XML 校验功能IDEA 里打开 mapper XML如果在某个下方画了红色波浪线说明解析器已经嗅到了问题。这种情况下不要急着写 SQL先把红线消掉再说。我还有一个习惯写动态 SQL 时每写一个if分支就顺手在注释里标注这个分支所对应的特殊字符场景例如这里条件较长用 CDATA 包住 。这样半年后自己回来看代码或者同事接手都能一眼明白当初的使用意图避免为了少写两行而随手替换掉正确写法。最后分享一个真实体会特殊字符处理这件事看着不起眼却是 MyBatis 项目里最能体现基本功的细节之一。我见过很多程序员能把复杂的多表关联写得飞起却在一条简单的create_time #{endTime}上报错半小时。记住一个原则——在 XML 里凡是可能被解析器误解的字符第一时间按纯文本处理而不是按 SQL 直觉处理。这样你在后续调试 MyBatis 时能省下大量无谓的排查时间。
返回列表