ARTICLE DETAIL

资讯详情

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

MyBatis mapper.xml中大于小于号转义与CDATA实战指南

MyBatis mapper.xml中大于小于号转义与CDATA实战指南 手写 mapper.xml 时很多人会在大于、小于、不等于号这里栽跟头。前几天还看到同组同事在SELECT ... WHERE count 1里直接写了和结果应用启动时 XML 解析直接报错。其实这个问题不复杂只是它横跨了三层语法XML 规范、MyBatis 的动态 SQL 解析、数据库的 SQL 语法。哪个环节没理顺就会写出一段看着没问题、跑起来报错的 XML。这篇文章就把转义、CDATA、OGNL 表达式、动态标签混用这些方法一次讲透给正在排查Invalid bound statement或者Error creating document的读者一个明确出路。1. 为什么mapper.xml里不能直接写小于号XML解析器的拆解逻辑1.1 一个典型的报错现场先还原一个非常经典的问题。假设我有这样一条查询select idlistByRange resultTypeUser SELECT * FROM user WHERE age 18 AND age 6 /select这段 SQL 光看语法MySQL、PostgreSQL 都能跑但在 mapper.xml 里MyBatis 启动加载的时候就会直接报类似这样的错误org.apache.ibatis.builder.BuilderException: Error creating document instance. Caused by: org.xml.sax.SAXParseException: 元素类型必须由匹配的结束标记终止原因就是 XML 解析器在读到WHERE age 18的时候把当成了 XML 标签的开始。它往后面找结果一直找到 SQL 末尾都找不到合理的标签结构于是抛出解析异常。也就是说这个错误根本轮不到数据库来执行MyBatis 在解析 mapper 文件的第一关就把它拦下来了。1.2和在XML规范里的不同待遇很多人第一反应是那我写成age 18怎么没报错这就要说到 XML 规范里几个符号的真实待遇了。XML 的标签结构由和界定所以在文本节点中是绝对禁止直接出现的它必须被转义也是禁止直接出现的因为它是实体引用的起始符号。而在文本节点里其实是可以直接出现的很多 XML 解析器不会因为报错。这也解释了为什么有的文件里写了能正常运行但同样的文件只要沾上就一定挂。不过能运行不意味着安全。在一些严格模式下后面如果紧跟]]之类的特殊序列依然可能被解析出歧义。所以专业的做法是除了等于号和不等号里不含 XML 特殊字符的部分其他比较符号一律统一处理。也就是说和都建议转义哪怕不转义通常也活得好好的但统一处理能让你的 mapper.xml 在任何解析器面前都不露怯。1.3 转义之后数据库才真正执行SQL这里要澄清一个常见的误解在 mapper.xml 里写了lt;最终发给数据库的是还是字符串lt;答案是。XML 解析器会在加载文档时把实体引用还原成对应字符所以 MyBatis 拿到的 SQL 已经是还原后的内容。举个例子WHERE age lt; 18MyBatis 解析后实际发送给 MySQL 的 SQL 片段是WHERE age 18理解这个还原机制很重要。它意味着你不用担心转义会不会把符号变成字符串导致查询条件失效你只需要担心在 XML 层写的符号是否合法。2. 转义、CDATA和OGNL文本运算符三种解法如何选2.1 转义实体最小改动但别把分号写丢最常见的方案就是 XML 实体转义。我先把常用映射列成一张表原符号转义写法使用场景lt;小于gt;大于lt;小于等于gt;大于等于lt;gt;不等于部分数据库写法!!不等于直接写即可amp;amp;动态SQL test属性中的逻辑与实际文件里的效果就是这样select idlistByAgeRange resultTypeUser SELECT id, name, age FROM user WHERE age gt; #{minAge} AND age lt; #{maxAge} /select这里有两个容易踩的细节。第一lt;最后的分号一定不能丢。写成了ltXML 解析器看到这种格式会直接报The reference to entity lt must end with ;的错误。这个错误特别容易在复制粘贴时出现因为脑子里想着小于号顺手就把分号忘了。第二lt;是小于等于的合法写法不是lt;lt;。有人会担心和两个符号都要转义结果写出了lt;lt;。这句话的意思完全变了。记住一点只需要转义本身是普通字符不需要也不应该转义。2.2 CDATA块一条大型比较逻辑的直接隔离当一段 SQL 里连续出现多个比较符号时逐个写转义符很累而且读起来难看得像编码题。这时候可以用 CDATA 区把整段逻辑包起来。select idfindProductsInPriceRange resultTypeProduct SELECT id, name, price FROM product ![CDATA[ WHERE price #{minPrice} AND price #{maxPrice} ]] /selectCDATA 区的含义是这里的字符都是纯文本XML 解析器不需要再做标签解析。所以里面的、、、都可以原样写MyBatis 会把 CDATA 里的内容原封不动地当作 SQL 文本对待。但这里要明确一件事CDATA 块内仍然可以使用#{}参数占位符。因为#{}是 MyBatis 自己的预编译参数标记不是 XML 标签CDATA 只拦 XML 解析器不拦 MyBatis 的后续处理。2.3 test属性里的OGNL文本运算符一种小众但好用的绕法如果你到现在还为if test...里的头痛那还有一个思路用 OGNL 自带的文本运算符把替换成单词lt把替换成gt。if testage ! null and age lt 18 AND age lt; 18 /ifOGNL 表达式里lt就是gt就是lte是gte是。这样在 test 属性里就完全不用写或了自然也不会碰到 XML 属性解析问题。不过我在实际团队里并不总是推荐这种写法原因很现实很多同事对 OGNL 的文本运算符不熟悉看到age lt 18会愣一下然后怀疑是不是自己少写了比较符号。如果你是一个人维护项目怎么方便怎么来如果是团队协作建议定规则不要混用。2.4 不等于号的正确打开方式标题里提到了不等于号这是三种符号里最简单的。因为!里没有任何 XML 特殊字符你可以直接写AND status ! #{excludeStatus}不需要转义也不需要包 CDATA。但有些同事习惯写在标准 SQL 里它也是不等于问题是在 XML 中它包含必须写成lt;gt;AND status lt;gt; #{excludeStatus}这写法虽然正确但可读性真的不行。更关键的是MySQL、PostgreSQL、Oracle 这些主流数据库都支持!。所以我建议团队统一用!少写三个字符也少一次转义风险。3. 动态SQL里最容易看走眼的两个位置test属性和SQL文本节点3.1 test属性里先经过XML属性解析再交给OGNL动态 SQL 里最让人混淆的地方就是if标签的test属性到底该不该转义。先说结论test属性首先是 XML 属性然后它的值才是 OGNL 表达式。所以 XML 层的要求依然适用。比如你想表达最大年龄小于 60下面这种写法是错的if testmaxAge ! null and maxAge 60因为test属性值里的在 XML 层面就已经不合法了。正确写法是if testmaxAge ! null and maxAge lt; 60MyBatis 在 XML 解析阶段会先把lt;还原成然后把maxAge 60这个字符串交给 OGNL 引擎求值最终得到 true 或 false。另外还要注意AND的写法。如果你在 test 里写 Java 常用的if testmaxAge ! null maxAge lt; 60同样会报 XML 解析错误因为是 XML 实体引用的起始字符。这里必须写成amp;amp;也就是if testmaxAge ! null amp;amp; maxAge lt; 60每次看到这种写法都觉得丑但这是最稳妥、最不出错的。如果团队嫌弃可以换成上面提到的 OGNL 文本运算符规避但一定要统一。3.2 SQL文本节点哪些地方该写lt;哪些地方该包CDATA在select、update这些标签的文本内容里转义和 CDATA 的取舍也是门学问。我的习惯是一个符号用转义多个连续符号用 CDATA但前提是不能破坏动态标签结构。看一个实际例子select idqueryUserList resultTypeUser SELECT id, name, age FROM user where if testminAge ! null AND age gt; #{minAge} /if if testmaxAge ! null AND age lt; #{maxAge} /if /where /select这里每个if里面只有一个比较运算符转义一下非常清晰利于后续同事维护。如果用 CDATA 也不是不行if testmaxAge ! null ![CDATA[ AND age #{maxAge} ]] /if但老实说在你只处理一个符号时CDATA 块反而增加了 tag 数量看起来笨重。CDATA 更适合那种整段没有动态标签、只有固定比较逻辑的场景例如一个固定的时间窗口查询select idqueryTodayData resultTypeStat SELECT * FROM stat ![CDATA[ WHERE create_time #{startTime} AND create_time #{endTime} ]] /select这种写法简洁而且不需要担心、、转义。关键就是别把if塞进 CDATA 里原因下一节专门讲。4. 踩坑复盘CDATA与if标签混用时条件为什么会集体失灵4.1 把动态标签塞进CDATAMyBatis自然看不到它们我在真实项目里见到过一种极端写法大概长这样!-- 错误示范别学 -- select idlistUser resultTypeUser ![CDATA[ SELECT id, name, age FROM user if testage ! null WHERE age #{age} /if ]] /select写这段代码的人的想法是CDATA 能保护不被 XML 解析器干扰那我就把整段 SQL 都包进去里面的if也一起放进去得了。但结果是什么CDATA 里的所有内容包括if、/if、test...都会被当作纯文本传给 MyBatis。MyBatis 根本不会把这一串字符串识别成动态标签最终生成的 SQL 大概长这样SELECT id, name, age FROM user if testage ! null WHERE age #{age} /if数据库看到if这串字面量直接报语法错误。如果你在错误堆栈里看到 SQL 中出现了if字样不用怀疑一定是有标签被误放进了 CDATA。正确的写法是动态标签必须放在 CDATA 外层CDATA 只保护某一个具体片段里的比较符号。或者反过来整个 SQL 都正常写只有出现特殊符号的单个条件用转义。下面这样是安全的select idlistUser resultTypeUser SELECT id, name, age FROM user where if testage ! null ![CDATA[ AND age #{age} ]] /if /where /select虽然 CDATA 块放在if内部看起来有点丑但逻辑上完全没问题。4.2lt;丢失分号、CDATA结束标记等拼写问题除了混放动态标签还有一些低级拼写错误会卡住人。最常见的是lt少了分号。前面提过XML 实体的完整写法是lt;、gt;少一个分号都会让解析器认为 entity 引用不完整。IDEA 一般会在 XML 文件里标红但如果你用的是文本编辑器或者没注意报错信息会非常抽象。第二个是 CDATA 的结束标记]]不能随便拆开也不能在中间加空格。我见过有人为了美观把]]前面的和 SQL 之间留了空格比如![CDATA[ AND age #{age} ] ]这样 CDATA 不会被正确闭合后面所有内容都可能被当作字符数据SQL 最终变得乱七八糟。CDATA 的格式必须是]]连续三个字符中间不能有任何空白。第三个是lt;和gt;后要不要加空格。这个问题不大但建议在转义符前后都留一个空格例如AND age gt; #{minAge}这样做的好处是 SQL 拼出来可读性好不容易把gt;和后面的#{minAge}糊在一起。5. 排错套路与一个完整示例看到报错后我通常按这个顺序查5.1 先看启动期解析错误还是先看SQL执行错误遇到 mapper.xml 相关的报错我一般分两种情况处理。第一种是应用启动阶段直接失败报XMLMapperBuilder、XMLStatementBuilder这类异常这时候几乎可以断定是 XML 文档本身的问题。我会优先检查文件里有没有未转义的、缺失的 CDATA 结束标记、写错的实体引用。这种问题不需要连数据库只要在 IDEA 里打开 XML 文件看标红位置就够了。第二种是应用启动正常但执行具体 SQL 时报BadSqlGrammarException或者MySQLSyntaxErrorException。这时候 XML 通常已经被正确解析了问题多半出在最终生成的 SQL 文本上。我会先把 SQL 打印出来MyBatis 开启mybatis.configuration.log-implStdOutImpl看比较符号是否被还原成、再看是不是某个参数拼错了位置。这里给一个简单的判断表报错类型优先怀疑位置排查动作SAXParseException出现在启动阶段XML 特殊符号、标签结构打开 XML 看标红对照转义表BadSqlGrammarException出现在执行阶段SQL 生成结果打印 SQL检查 CDATA 和动态标签拼接NumberFormatException或OGNLExceptiontest 属性的类型比较检查 OGNL 表达式中参数类型是否匹配5.2 一个左闭右开区间查询的标准写法把所有规则放在一起最典型的实战场景就是时间范围查询。需求是查创建时间大于等于开始时间并且小于结束时间同时排除 status 等于某个值的用户。select idsearchUsers resultTypeUser SELECT id, name, status, create_time FROM user where if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if if testexcludeStatus ! null AND status ! #{excludeStatus} /if /where /select这段代码里gt;还原成lt;还原成!直接写。三个比较符号分别用了不同的处理方式但每一种在 XML 层面都是合法的。选择左闭右开区间也是刻意的。create_time startTime AND create_time endTime能避免同一条数据在上一页结束时间和下一页开始时间重合时被查出来两次。这个细节和符号转义同样重要因为很多人把符号写对了却把边界条件写错了。6. 团队约定把特殊符号的写法变成规范而不是个人习惯6.1 我建议固定下来的三条规则经历过几次帮别人排查 mapper.xml 特殊符号报错之后我强烈建议团队在代码规范里把符号写法明确下来。否则每次换人写 XML都会冒出新的风格排查成本非常高。我目前的团队约定是三条SQL 片段里的单个比较符号统一用实体转义例如gt;、lt;。连续出现三个以上固定符号且没有动态标签时才允许用 CDATACDATA 内禁止出现 MyBatis 动态标签。动态 SQL 的test属性统一使用转义后的 XML 实体例如testmaxAge ! null and maxAge lt; 60。不推荐在团队代码里使用 OGNL 的lt、gt文本运算符因为可读性差除非有同事坚持且全员评审通过。不等于号统一写!禁止写。这样既避免转义也减少一种没有必要存在的写法。这套约定可能不是最优解但它是最容易形成肌肉记忆的版本。别人接手你的 mapper 文件时不需要思考这里为什么用 CDATA那里为什么用转义照着规则走就行。6.2 IDEA的XML检查能帮你提前止损最后分享一个提高效率的小习惯使用 IDEA 打开 mapper.xml 文件时不要忽略编辑器的标红提示。IDEA 的 XML inspector 对实体引用非常敏感。如果你写了lt而忘了分号它会立刻标红如果你写了不合理的它也会提示 XML tag has no end tag 之类的问题。启动应用前先扫一眼文件有没有红色波浪线能帮你省掉至少十分钟的启动排错时间。另外开启 MyBatis 的 SQL 日志输出也很重要。把mybatis.configuration.log-impl设置为StdOutImpl后每次真正发给数据库的 SQL 会在控制台打印出来。这样你能第一时间确认lt;是否被正确还原成了而不是凭空猜测。我自己实际写 mapper.xml 的时候已经形成了固定动作先写 SQL再检查一次有没有、和然后根据符号数量决定用转义还是 CDATA。这个习惯踩过几次坑之后才真正建立起来。如果你也在为这些特殊符号头疼照着上面的方法处理大概率不会再被 XML 解析器卡住。
返回列表