
如果只看标题你可能会觉得今天的内容过于基础 —— XML这不是比 JSON 还“老”的东西吗但真正进了项目就会发现老技术从来不等于没问题。Spring Boot 里 MyBatis 映射文件该放哪儿、怎么配才能用一个看起来正常的 XML 解析接口为什么被别人一个请求就读走了服务器文件甚至工业软件里西门子 TIA Openness 的项目结构也在用 XML 做数据交换。这些坑我都踩过也见过不少新同事在同样地方反复打转。这篇 day36-xml 的学习笔记就把我从基础解析、编辑器选择到 MyBatis-Plus 配置、XXE 安全防御的一整套经验和踩坑记录整理出来希望能帮你少走几步弯路。1. XML 到底解决了什么问题为什么还在学1.1 XML 的核心形态一个配置狂魔的自白XMLExtensible Markup Language是一种可扩展标记语言它解决的核心问题只有一个用一种自描述的、严格的文本格式来表达结构化的数据。这里的“严格”是双刃剑严格意味着机器可以校验和解析也意味着你写错一个尖括号、一个大写字母整个程序都可能崩掉。一个最小的 XML 长这样?xml version1.0 encodingUTF-8? order id1001 customer name张三/name phone13800000000/phone /customer items item skuA001 quantity2机械键盘/item item skuA002 quantity1显示器支架/item /items /order结构上一眼就能看明白根元素order包裹所有内容customer和items是子元素id、sku是属性。和 HTML 不同XML 的标签不是预定义的你可以根据数据模型随便造所以它天生适合做“配置文件”和“消息交换格式”。再加上 DTD文档类型定义或 XSDXML Schema你还能强制规定每个元素出现几次、类型是什么把数据准确性卡得死死的。顺带一提很多年后端老系统非常依赖这套“严格校验”。比如你调用某些老牌 ERP 的 OpenAPI对方接口要求的就是 SOAP 协议里的 XML 消息客户名称、物料编码、单据类型哪个字段缺失或类型错误立刻报 Fault。这类场景里 JSON 反而没 XML 灵活因为 SOAP 的规范里带了完整的错误码和扩展机制没有 XML 这套标签和命名空间很难做到这种程度。我最初也觉得“都什么年代了还用 SOAP”但真对接过一两个传统制造企业的 U9C 开放接口后就明白了在老工业软件和大型企业集成里XML 至今仍是事实标准。1.2 为什么 JSON 这么火XML 还是没被淘汰JSON 得益于体积小、和 JavaScript 天然契合、心智负担低已经成为 Web API 的默认选择。但 XML 在某些领域依然活得很好甚至无可替代。特性XMLJSON数据类型表达纯文本元素/属性需配合 XSD 约束原生支持字符串、数字、布尔、数组可扩展性支持命名空间、DTD/XSD、XPath/XSLT较弱没有标准查询语言元数据表达属性比子元素更适合描述“数据的描述”用嵌套对象实现结构容易臃肿工具链生态极度成熟从解析到校验一应俱全轻量但复杂查询需要额外方案典型场景配置文件、SOAP 协议、Office 文档底稿、工业软件数据交换REST API、前端状态、移动端通信最典型的例子是 Office 的 docx/xlsx 文件本质就是一个 zip 压缩包里面塞了几十个 XML 文件。你要用代码改一个 Word 文档的页眉页脚、Excel 的单元格样式绕不开 OpenXML 里的那些 XML 节点。还有 Java 的 Maven 配置用的是 XMLSpring 早期的 Bean 定义也是 XML工业自动化里的西门子 TIA Openness 也可以通过 XML 描述项目结构——下载学习它的 Openness 示例工程你会发现大量 XML 文件。所以我的观点很明确你可以不喜欢 XML但不能不会读、不会配、不会防它出问题。越是基础且普及的技术一旦出错影响面越广。Day 36 这个节点学 XML不是翻旧账而是在给后续的项目实战补地基。2. 打开、编辑和解析 XML从入门到顺手2.1 用什么工具读写 XML 才不痛苦很多人被 XML 劝退是因为最初用记事本打开一个几百行的 XML全挤在一行眼睛直接瞎了。其实工具选对体验完全不差。VS Code安装扩展 “XML Tools” 或 “XML Language Support”格式化ShiftAltF、校验、XPath 查询都有。也有项目会配 XML 目录结构这边不细说。IDEA / JetBrains 全家桶自带 XML 高亮和格式整理。如果你在写 MyBatis 的 mapper XML强烈建议装一个MyBatisX插件。装完后 mapper XML 的方法名、参数引用都是高亮绑定点一下还能跳转到对应的 Mapper 接口方法。早期没有这个插件的时候我每次改 SQL 只敢全局搜索方法名效率极低后来有插件了简直救命。Notepad轻量查看和快速替换支持语法高亮就是插件生态老一点。适合马上改一个临时文件不用打开重型 IDE。XMLSpy / Oxygen XML Editor专业 XML 编辑工具对 XSD 校验、XSLT 调试支持极好适合出版社、金融报文、政企对接这类需要严格校验的领域。日常写代码不需要上这么重。编辑器里还有个容易被忽略的高亮点MyBatis XML 的高亮不只是颜色好看更是查错的工具。比如#{}和${}如果用错了插件往往能直接标出提示XML 标签闭合有问题也能第一时间看到红色波浪线。我见过一个同事XML 里少写了一个/if自己盯了半小时没发现IDEA 一眼就标出来了。所以从第一天做项目开始就不要用记事本硬刚 XML。2.2 DOM、SAX、StAX三种解析模型怎么选聊到解析就绕不开三个经典模型。很多人刚学时容易混淆我换个方式说。解析模型工作方式优势劣势推荐场景DOM一次读入并构建完整树对象使用简单可增删改查大文件吃内存明显配置文件、中小型文档SAX事件驱动边读边触发回调速度快内存占用小不能倒退代码逻辑复杂超大 XML、流式处理StAX拉式解析由代码主动取节点灵活、可暂停同样需要自己维护状态高性能解析、增量处理典型 Java 里用DocumentBuilderFactory来做 DOM 解析把整个 XML 变成Document对象然后用getElementsByTagName去查节点。这种方式对小配置非常友好。DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(new File(order.xml)); NodeList customers doc.getElementsByTagName(name); for (int i 0; i customers.getLength(); i) { System.out.println(customers.item(i).getTextContent()); }Python 更简单标准库的xml.etree.ElementTree可以一行读取import xml.etree.ElementTree as ET root ET.parse(order.xml).getroot() for name in root.iter(name): print(name.text)如果不求性能开发阶段用这些完全够了。但如果你要解析的是 GB 级别的业务报文DOM 直接会让内存爆掉必须换 SAX 或 StAX。我记得有一次处理银行流水对账文件几个 GB 的 XML一开始用 DOM 只跑了几分钟就 OOM后来切了 StAX 流式解析内存占用从 2G 降到 300M这就是模型选型的重要。2.3 实战解析Python 和 Java 各来一发上面代码算是最小示例这里补一个真实感强一点的。假设我们要从商品 XML 里读出所有 SKU 为A001的库存数量import xml.etree.ElementTree as ET xml_data inventory product skuA001 name机械键盘/name stock15/stock /product product skuA002 name显示器支架/name stock7/stock /product /inventory root ET.fromstring(xml_data) for product in root.findall(product): if product.get(sku) A001: stock product.find(stock).text print(f机械键盘库存: {stock})如果 XML 里还有命名空间findall时就要注意前缀。很多新手在这个位置被坑过XML 开头明明写了xmlns:nshttp://xxxPython 里却直接用ns前缀查找节点结果 parse 返回空。正确做法是把命名空间单独映射一下ns {ns: http://xxx} root.findall(.//ns:product, ns)Java 这边我建议别直接用底层 DOM 写太多样板代码Java 8 配合 XPath 更省心DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(new FileInputStream(inventory.xml)); XPath xPath XPathFactory.newInstance().newXPath(); String stock xPath.evaluate(/inventory/product[skuA001]/stock/text(), doc); System.out.println(stock);这一小段看起来简单但请注意DocumentBuilderFactory.newInstance()这一步稍后讲到安全时你还会见到它。不同的newInstance()默认配置行为可能不一样尤其在 JDK 高版本和低版本之间配置外部实体的策略也不完全相同。所以解析器代码宁可写显式 setFeature也不要偷懒用默认。3. Spring Boot MyBatis-Plus XML 映射文件从配置到上线3.1 XML 和 Mapper 接口要不要放同一个目录这是最近后台提问里出现频率超高的问题“Spring Boot 项目里用了 MyBatis-PlusXML 和 Mapper 接口放同一个文件夹下应该怎么配置”我先给结论技术上完全可以但没特殊理由不建议非要把 XML 放 src/main/java 下面。原因是 Maven 默认打包时src/main/java目录只打包.java源文件不会把.xml文件复制到target/classes。你把 XML 放再对的位置不配置额外资源路径运行时就是找不到映射文件Mapper 方法直接报Invalid bound statement (not found)。如果你的项目里所有 Mapper 接口都放在com.example.demo.mapper包下为了查看方便硬要把 XML 也放在这个目录里那么必须做两件事在pom.xml里告诉 Maven打包时把src/main/java下面的.xml文件也当作资源。在application.yml里把mapper-locations指到对应路径。这样做确实让 Mapper 接口和 XML 在目录结构上处于同一包开发者不用切资源目录就能看 SQL。但代价是每次新增 Mapper 模块都要注意配置对不对一旦忽略要么本地能跑但打 jar 后找不到 XML要么 CI 里莫名其妙报错。我个人实验下来的最终结论是放 resources 目录才是标准姿势。同目录方案适合学习实验、单模块小项目生产环境还是按社区规范来效率最高。3.2 亲手把同目录配置跑通如果你现在确实需要把 XML 和 Mapper 接口放在同一个包下可以按下面的步骤走一遍。第一步项目里创建 Mapper 接口package com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; Mapper public interface UserMapper extends BaseMapperUser { User selectByName(String name); }第二步在同包目录下建同名 XML 文件目录结构如下src/main/java/com/example/demo/mapper/ ├── UserMapper.java └── UserMapper.xmlUserMapper.xml的namespace必须是接口全限定名?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectByName resultTypecom.example.demo.entity.User SELECT * FROM user WHERE name #{name} /select /mapper第三步pom.xml 添加资源打包配置build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build注意第一个resource会把src/main/java下的 XML 全部拉进打包产物同时保留原有的src/main/resources资源目录。否则你只写第一个resource会覆盖掉默认资源目录导致application.yml等文件也丢了。第四步application.yml 里指定 mapper-locationsmybatis-plus: mapper-locations: classpath*:com/example/demo/mapper/*.xmlclasspath*:前缀的意思是扫描所有 jar 包资源路径如果有多模块项目这个写法更稳妥。单模块直接用classpath:也行但最好保持一致。这里有一个易错点如果你在pom.xml已经配置了把src/main/java内的 XML 打包到target/classes那么最终产物里com/example/demo/mapper/UserMapper.xml与com/example/demo/mapper/UserMapper.class会出现在同一目录下mapper-locations用classpath*:com/example/demo/mapper/*.xml才能正确匹配。过一遍这些配置后启动项目看到日志里有Loaded mapper ...或 SQL 正常执行就说明整个链路通了。用 MyBatisX 插件的话Mapper 接口方法左侧会有一个小图标点击能直接跳到对应 XML 语句这就是高亮和导航的价值。3.3 映射 XML 里的隐藏扣分点配置只是第一步真正在 XML 里写 SQL 才是天天踩雷的地方。给你分享几个我印象最深的问题。第一XML 特殊字符必须转义。SQL 里常见的是 XML 的标签标志直接写在 SQL 里会破坏 XML 结构。比如select * from user where age 18在 XML 里必须写成age gt; 18或者用![CDATA[包起来。select idselectAdult resultTypeUser ![CDATA[ select * from user where age 18 and create_time now() ]] /selectCDATA 块里的内容会当成纯文本处理这是最直观的解决方案。但注意 CDATA 不能直接套在动态 SQL 标签外面比如if内部用![CDATA[是可以的可如果 CDATA 把if也包进去动态标签就失效了。第二#{}和${}差别要牢记。#{}是预编译参数占位符最终生成?占位符可以防 SQL 注入${}是字符串拼接直接把内容塞进 SQL。MyBatis-Plus 的分页插件在某些场景下要拼接 order by 字段可能不得不用${}但只要能传参数一律优先用#{}。我之前审计代码时见过有人在 XML 里写order by ${sortField}这个sortField还是前端传的非常危险。第三resultType 和 resultMap 不是一回事。如果查询结果要关联嵌套对象或者数据库字段下划线与 Java 属性驼峰不一致需要配置map-underscore-to-camel-case: true或显式使用resultMap。很多人只配了map-underscore-to-camel-case就以为所有字段都自动映射了遇到复杂 DTO 照样报“绑定异常”。正确做法是查简单实体类用 resultType 下划线映射配置查连表、聚合、一对一、一对多直接写 resultMap一清二楚。4. 被忽略的 XML 安全问题从 XCTF 题目看 XXE4.1 Fake XML Cookbook 是如何被打穿的[XCTF/NCTF2019] Fake XML Cookbook 是一道很经典的题目它模拟了一个“XML 食谱”应用用户提交一段 XML应用会解析并把菜名等信息返回。表面上看这是一个学习 XML 解析功能的练习环境但实际上它暴露了 XML 领域最著名的安全漏洞之一 ——XXEXML External EntityXML 外部实体注入。我强调一下以下内容仅用于安全学习和授权测试。如果拿到自己或已授权的实验环境你可以尝试在提交的 XML 里插入一段外部实体定义?xml version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] cookbook recipe namexxe;/name /recipe /cookbook如果后端解析器没有禁用外部实体xxe;会被替换成服务器/etc/passwd的内容然后应用把name节点的值返回给前端你就相当于拿到了一台服务器的读取权限。这就是题目里 “Fake XML Cookbook” 的考点表面在做饭实际上在漏文件。除了file://协议常见的还有php://filter/readconvert.base64-encode/resourceindex.phpPHP 场景下读源码。http://内网地址/做 SSRF探测内网端口。expect://id如果目标环境 PHP 装了 expect 扩展可能甚至执行命令。问题根因很简单XML 规范允许通过 DTD 声明外部实体很多解析器为了兼容旧应用默认开启了外部实体加载。攻击者只要找到一个能提交 XML 数据的入口就能利用这一点。4.2 给 XML 解析器上三道锁我在平时开发里凡是接收外部 XML 的接口都会严格处理解析器配置。因为不同框架默认策略不一样所以最稳妥的做法是显式禁用外部实体。Java 里最常用的DocumentBuilderFactory需要设置多项 featureDocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); // 禁用 DTD dbf.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); // 禁用外部普通实体和外部 DTD dbf.setFeature(http://xml.org/sax/features/external-general-entities, false); dbf.setFeature(http://xml.org/sax/features/external-parameter-entities, false); // 关闭外部 DTD 加载 dbf.setFeature(http://apache.org/xml/features/nonvalidating/load-external-dtd, false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false);disallow-doctype-decl是最狠的一道锁直接拒绝包含 DOCTYPE 声明的文档。大多数业务场景根本不需要用户传 DTD遇到直接抛出异常即可。如果你基于 SAX 解析同样有对应的XMLReader配置。Python 里用lxml时如果版本足够新默认不会加载外部实体但安全起见可以显式禁用from lxml import etree parser etree.XMLParser(resolve_entitiesFalse, no_networkTrue, dtd_validationFalse) root etree.fromstring(xml_data, parserparser)不要用from lxml import etree时的默认解析器直接解析不可信 XML默认行为在不同版本间可能变化。核心心法就一句谁传 XML 给你你就假设这个人想搞你解析前先堵死所有外部资源加载路径。4.3 MyBatis XML 也怕 XXE 吗这个问题经常被搞混。有人看到安全通告说“XML 解析漏洞”立刻怀疑自己项目里的 MyBatis mapper XML 是不是也有风险。答案很清晰MyBatis 的 mapper XML 是开发者自己写在代码库里的静态资源不是用户输入因此不可能被外部用户利用来注入 DOCTYPE。XXE 攻击成立的前提是解析“不可信的 XML 内容”比如从 HTTP 请求体、上传文件、接口报文中直接读取的 XML。而你项目里的 mapper XML 是自己人写的打包后放在 classpath 下解析它时不会有用户的输入混进去。但这不代表你能彻底松懈。如果项目里某个接口接收客户端上传的 XML再用jackson-dataformat-xml或 JAXB 反序列化成 Java 对象那就要检查是否安全配置了。还有一些企业集成场景比如对接 SOAP 接口时客户端发来的 SOAP Envelope 本身也带 DOCTYPE 的隐患。所以我的习惯是凡是“入站 XML”统一走同一个安全解析工具类不搞特例。这也是我在做完 Fake XML Cookbook 题目后最大的感触很多安全问题不是新知识而是旧技术里被忽略的默认行为。你多花十分钟配置一套安全解析器可能就堵住了一个能泄露服务器文件的大口子。5. XML 实操踩坑记录与排查速查5.1 记忆犹新的四个坑坑一Windows 记事本编辑 XML莫名多了两个字符。说起来很气用记事本保存 UTF-8 编码的 XML 时Windows 会自动在文件开头加上 BOMByte Order Mark。Java 的某些 XML 解析器不认 BOM会报类似Content is not allowed in prolog的错误。排查半天才发现是文件编码问题。解决办法是用 VS Code、Notepad 重新编码为 UTF-8 无 BOM。后来我所有 XML 文件都强制默认无 BOM这条规则一直沿用至今。坑二多模块项目里 mapper-locations 通配符写错。假设子模块 A 的 mapper 在classpath:mapper/*.xml子模块 B 的 mapper 也在自己模块的resources/mapper下Spring Boot 启动时可能只扫描到其中一个模块。把配置改成classpath*:mapper/*.xml后问题才消失。这个*的区别很微妙classpath:只扫描当前项目 classpath 下的第一个匹配路径classpath*:会扫描所有 jar 和依赖里的匹配路径。多模块项目一定要用后者否则你会陷入“本地单模块跑得好好的拆成微服务就找不到 XML”的尴尬。坑三XML 里写了中文注释保存时编码不对。如果你的 XML 头声明?xml version1.0 encodingUTF-8?但文件实际用 GBK 保存解析时会报编码错误或中文乱码。编辑器右下角改下编码重新保存就可以。这个坑虽低级但在 Windows 下团队协作时特别容易出现也要在代码评审时注意养成“统一 UTF-8”的规范。坑四MyBatis 的 namespace 和接口全限定名对不上。这是BindingException最常见的根源。如果你复制粘贴一份 XML忘记改namespace那不管怎么配置 mapper-locations接口方法都找不到对应 SQL。排查时可以看启动日志中 MapperFactoryBean 的扫描记录或直接用 MyBatisX 跳转检查。高亮插件在这里的真正意义就是让你肉眼看到接口方法没有绿色导航线时第一时间反应“命名空间是不是写错了”。5.2 一套快速定位问题的姿势遇到 XML 相关异常我一般按以下顺序排查确认能不能解析把 XML 内容单独复制到一个临时文件用浏览器打开或 IDEA 里格式化快速定位结构错误。浏览器直接打开 XML 报错行数通常很明确。确认编码与 BOM检查文件编码是否与 XML 头一致有无 BOM。确认路径扫描Spring Boot 项目启动时如果提示Invalid bound statement先用jar tf看一下打包产物里到底有没有 XML 文件。确认 SQL 语法MyBatis 只负责把 XML 里的 SQL 语句发到数据库如果 SQL 语法错误数据库会报错检查日志里Preparing:后拼接出来的 SQL。确认参数绑定多个参数时XML 里写#{0}、#{1}在老版本有效新版本推荐用Param明确命名否则容易绑定到错误的参数。我把排查要点整理成了表格你可以直接存下来对照。异常现象常见原因排查方向Content is not allowed in prolog文件头 BOM 或不可见字符用编辑器另存为 UTF-8 无 BOMInvalid bound statementXML 未打包或 namespace 不匹配看 target/classes 有没有 xml检查 namespaceNo such property: xxx参数名不对或缺少 ParamXML 参数名与接口一致XML 解析器报 Entity 禁用代码里显式禁止 DTD 导致与旧配置冲突确认业务是否必须传 DTD非必要保持禁用中文乱码文件编码与解析器不一致统一 UTF-8 编码附送一个小技巧在application.yml里配置 MyBatis 的日志输出能极大提升排查效率。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每次执行 SQL控制台会打印完整的预处理语句和参数列表你就能一眼看出#{}有没有被正确填充${}有没有拼接异常。最后再分享一个我自己养成的小习惯项目里加一个简单的 XML 工具集合统一封装“解析不可信 XML”的安全配置。团队其他人要解析 XML 时不允许直接在业务代码里DocumentBuilderFactory.newInstance()而是调用这个工具方法。这样即使有人不熟悉 XXE也不会写出默认配置的解析器。像 XML 这种越基础的技术越要有一套默认的安全实践和标准配置才能真正少踩坑。