1. MyBatis XML中特殊符号的困境第一次在MyBatis XML映射文件里写SQL时我踩了个大坑。当时需要查询某个时间范围内的订单数据很自然地写下了WHERE create_time #{startTime} AND create_time #{endTime}。结果程序一运行就直接报错控制台抛出一堆XML解析异常。后来才知道原来和这些符号在XML里有特殊含义直接使用会导致解析器误认为是标签的开始。这个问题在实际开发中特别常见。比如做数据筛选时要用到比较运算符处理位运算时会用到符号甚至简单的WHERE 11这种条件都可能出问题。XML解析器看到这些特殊字符时会试图将它们解释为标签或实体引用而不是普通的SQL语句内容。2. 转义字符方案详解2.1 常用HTML实体转义表最直接的解决方案就是使用HTML实体转义。下面这个表格是我整理的MyBatis中最常用的转义对应关系原始字符转义写法使用场景示例lt;WHERE age lt; 18gt;WHERE salary gt; 10000amp;WHERE flag amp; 1 1quot;字符串中的双引号apos;字符串中的单引号2.2 实际开发案例上周我刚用转义方案解决了一个实际问题。需求是要查询过去30天内未处理的工单SQL条件需要同时使用大于和小于号select idfindPendingTickets resultTypeTicket SELECT * FROM ticket WHERE status PENDING AND create_time gt; DATE_SUB(NOW(), INTERVAL 30 DAY) AND create_time lt; NOW() /select这里特别注意两点转义字符结尾的分号不能省略gt缺少分号是无效的转义后的SQL在日志中显示的是转义前的字符不影响可读性3. CDATA区方案深度解析3.1 CDATA的本质与语法当SQL语句中包含大量特殊字符时逐个转义会很麻烦。这时可以用![CDATA[ ]]包裹整个SQL片段。CDATA是XML的语法特性它会告诉解析器这个区域里的内容请原样处理不要解析任何标签或实体。我常用的写法模式是这样的select idcomplexQuery resultTypeMap ![CDATA[ SELECT u.id, u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id o.user_id WHERE u.register_time #{startDate} AND u.register_time #{endDate} AND (u.status 1) 1 GROUP BY u.id HAVING order_count 5 ]] /select3.2 使用时的注意事项在实际项目中我发现CDATA区有几个需要特别注意的地方范围控制不要无脑包裹整个SQL应该只包含确实需要转义的部分。比如下面这个例子就处理得很好select idfindProducts resultTypeProduct SELECT * FROM products where if testcategory ! null AND category #{category} /if if testminPrice ! null ![CDATA[ AND price #{minPrice} ]] /if if testmaxPrice ! null ![CDATA[ AND price #{maxPrice} ]] /if /where /select动态SQL冲突CDATA区内不能包含MyBatis的动态SQL标签如if、foreach否则这些标签会被当作普通文本格式化问题CDATA区内的换行和缩进会被保留可能影响SQL的可读性4. 两种方案的对比与选型4.1 性能与可读性分析经过多次实际项目验证我发现两种方案各有优劣转义字符方案优点精确控制转义范围与动态SQL完美配合缺点复杂SQL中大量转义字符影响可读性适用场景简单条件判断、少量特殊字符的情况CDATA区方案优点保持SQL原生写法特别适合复杂查询缺点不能与动态SQL标签混用适用场景固定SQL片段、包含多个特殊字符的复杂查询4.2 混合使用的最佳实践在很多场景下我会组合使用两种方案。比如这个统计报表查询select idgetSalesReport resultTypemap ![CDATA[ SELECT DATE_FORMAT(o.create_time, %Y-%m-%d) AS day, COUNT(*) AS total_orders, SUM(o.amount) AS total_amount FROM orders o WHERE o.create_time #{startDate} ]] if testregion ! null AND o.region #{region} /if ![CDATA[ AND o.create_time #{endDate} AND o.status 1 0 GROUP BY day HAVING total_amount 10000 ]] /select这种写法既保留了关键条件的可读性又能灵活使用动态SQL。5. 特殊场景处理技巧5.1 位运算的特殊处理处理位运算时符号需要特别注意。我曾经遇到过这样的错误写法!-- 错误示例 -- select idfindByFlag resultTypeUser SELECT * FROM users WHERE flag #{mask} #{value} /select正确的写法应该是select idfindByFlag resultTypeUser SELECT * FROM users WHERE flag amp; #{mask} #{value} /select5.2 时间范围查询的优化时间范围查询是最常用到比较运算符的场景。我总结了几种优化写法!-- 方案1转义字符 -- if teststartTime ! null AND create_time gt; #{startTime} /if !-- 方案2CDATA区 -- if testendTime ! null ![CDATA[ AND create_time #{endTime} ]] /if !-- 方案3使用MyBatis的转义方法 -- if testorg.apache.commons.lang3.StringUtilsisNotBlank(timeRange) AND create_time ${com.example.MyBatisUtilsescape(timeRange)} /if6. 常见错误排查指南在团队代码审查中我发现以下几个典型错误出现频率最高遗漏转义忘记转义或导致XML解析错误错误现象启动应用时直接报XML解析异常解决方案检查SQL中的特殊字符是否全部正确转义CDATA区滥用将整个SQL包含在CDATA中导致动态SQL失效错误现象if等条件判断不生效解决方案只对确实需要转义的部分使用CDATA转义字符格式错误常见错误写成gt缺少分号、amp缺少分号正确写法必须包含结尾分号如gt;、amp;混合编码问题现象部分编辑器会自动转换特殊字符解决方案统一文件编码为UTF-8关闭自动转换功能7. 开发工具支持好的工具能大幅提升效率。我常用的几个工具技巧IDE插件IntelliJ IDEA的MyBatis插件能自动提示转义字符Eclipse的XML编辑器可以高亮显示未转义的特殊字符代码模板 我配置了几个Live Template输入mygt自动生成gt;输入mycdata生成完整的CDATA区块验证方法在Mapper接口方法上右键Test直接运行测试开启MyBatis的SQL日志确认生成的SQL符合预期团队规范在项目README中明确转义规则使用Checkstyle插件检查XML文件中的特殊字符代码审查时特别注意SQL片段的写法8. 实际项目经验分享去年在电商项目中我们遇到了一个复杂的商品筛选需求。用户可以通过多个维度组合筛选商品包括价格区间、上架时间、库存数量等。最初的实现是这样的select idsearchProducts resultTypeProduct SELECT * FROM products where if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testcategoryIds ! null AND category_id IN foreach collectioncategoryIds itemid open( separator, close) #{id} /foreach /if ![CDATA[ AND (stock gt; 0 OR is_preorder 1) ]] /where if testorderBy ! null ORDER BY ${orderBy} /if /select这个实现有几个值得注意的点简单条件使用转义字符方案复杂条件使用CDATA区动态SQL标签与特殊字符处理完美结合排序字段使用${}直接拼接注意SQL注入风险经过多次迭代我们最终形成了一套团队规范简单比较使用转义字符复杂逻辑判断使用CDATA所有XML文件必须通过Checkstyle验证定期检查MyBatis日志中的实际执行SQL