ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MyBatis XML 报错 DTD 加载失败?一文讲清 IDE 校验原理与本地映射方案

MyBatis XML 报错 DTD 加载失败?一文讲清 IDE 校验原理与本地映射方案

早上打开工程,mybatis-config.xml第一行那条<!DOCTYPE ...>上顶着一道红色波浪线,鼠标悬停弹出:Referenced file contains errors (http://mybatis.org/dtd/mybatis-3-config.dtd)。你顺手点了一下运行,控制台 SQL 照样打印,增删改查一个不差,数据库连接池也正常初始化。这时候大部分人的第一反应是去动 pom、换 MyBatis 版本、清 IDEA 或 Eclipse 的缓存,甚至把配置文件整个重写一遍——我见过最极端的一位直接把 XML 删了改纯 Java 配置,折腾两个多小时,红叉依然挂在那里。

先把结论摆在前面:这条报错绝大多数情况下不是 MyBatis 的问题,也不是你的 XML 写错了,而是 IDE 的 XML 校验器在替你检查 DOCTYPE 时没能拿到那份 DTD。MyBatis 自己在运行期根本不联网,它内部注册了一个 EntityResolver,直接去 jar 包里读同名的 DTD 文件做解析。所以"运行正常但编辑器报错"这个组合,本身就是最有力的线索。

这篇内容适合三类人:刚接手一个老 SSM 项目、被满屏红叉吓到的新同学;在内网、离线、代理受限环境里搭环境的中高级开发;以及带团队、需要给工程定一套"打开就干净"规范的技术负责人。下面我把这条报错的来龙去脉、三类根因、离线映射方案、以及几个相邻的坑,按我实际排查的顺序完整讲一遍,照着做基本能一次解决。

1. 先搞清楚是谁在喊:报错来自 IDE,不是来自 MyBatis

这一步看着像废话,但它决定了你后面所有动作的方向。很多人一看到 DTD 三个字就往 jar 包和依赖上想,结果方向从一开始就偏了。

1.1 DOCTYPE 里那一长串到底是什么

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">这行声明包含两个关键标识:PUBLIC 后面的公共标识符(那串-//mybatis.org//DTD Config 3.0//EN)和后面的系统标识符(那个 http 地址)。XML 解析器看到 DOCTYPE,就会尝试加载对应 DTD,用它来判断元素名合不合法、子元素顺序对不对、属性有没有拼错。

问题就出在"尝试加载"这四个字上。一个"老实"的解析器会真的去访问那个 http 地址;而绝大多数 IDE 的校验器会先查本地缓存和目录映射,查不到才走网络,网络不通就直接把整条 DOCTYPE 标成错误。于是你看到的报错文本其实是"这个被引用的文件(也就是 DTD)本身有问题",而不是"你的配置写错了"。这句话的措辞误导了太多人。

1.2 MyBatis 运行期为什么完全不受影响

MyBatis 在构建XMLConfigBuilder的时候,用的是内部自带的XMLMapperEntityResolver。这个类里维护了一张固定的映射表,把公共标识符直接指到 jar 包路径下的org/apache/ibatis/builder/xml/mybatis-3-config.dtd。也就是说,你在 XML 里写的那个 http 地址,在 MyBatis 眼里只是个"用于匹配的字符串",它压根不会拿它去发请求。

mapper 文件同理,-//mybatis.org//DTD Mapper 3.0//EN会被映射到 jar 里同目录的mybatis-3-mapper.dtd。所以哪怕你把网线拔了,只要 jar 在 classpath 上,MyBatis 该跑还是跑。这也解释了为什么很多纯内网项目跑得好好的,只是编辑器一直红着。

1.3 三类现象对照表:先给自己定位

在动手之前,先用下面这张表给自己归个类,后面的方案才能对症。我用这几个判据帮同事排查过太多次,准确率相当高。

现象最可能的根因该走哪一节
报错只出现在编辑器,运行日志一切正常IDE 取不到远程 DTD(离线/内网/跳转失败)第 3 节的本地映射方案
报错同时提示"元素顺序必须匹配"DTD 里的 sequence 约束被违反第 4 节的顺序表
新加某个标签就红,老标签正常IDE 缓存的 DTD 版本比 jar 旧第 2.3 节的缓存比对
换台机器就正常,本机一直红本机缓存/网络环境差异第 2.1 节先测连通性

提示:先别急着改文件。把工程跑一次,确认业务逻辑正常——这一步能帮你排除掉"配置真的写错了"这个可能,省下大量瞎改的时间。

2. 三类根因对号入座,别一上来就动配置

我排查这类问题有个固定习惯:先证伪,再修正。下面三条路径,基本覆盖了我遇到过的九成以上情况。

2.1 第一种:DTD 就是拉不下来

这是最常见的一类,尤其在企业内网、隔离环境、或者需要经过中间层出网的环境里。判断方法很简单:直接把http://mybatis.org/dtd/mybatis-3-config.dtd粘到浏览器地址栏里打开。

能正常显示一大段<!ELEMENT ...>文本,说明网络没问题,那问题在别处;转圈半天打不开、返回 301/302 跳转、或者干脆白屏,那基本可以确认了。这里有个容易被忽略的细节:不少环境会把 http 请求强制跳到 https,而某些版本 IDE 的解析器不跟随跳转,于是它拿到的是个重定向响应而不是 DTD 内容,报错就来了。所以"浏览器能打开"不等于"IDE 能取到",浏览器会跟随跳转,IDE 不一定。

这一类的正确解法不是在 XML 里折腾,而是把 DTD 请到本地,再让 IDE 认识它,具体见第 3 节。

2.2 第二种:元素顺序踩了 DTD 的 sequence 约束

这一类特别有迷惑性,因为报错文本长得跟第一类几乎一样,很多人会误以为是没下到 DTD。实际上mybatis-3-config.dtd里对根元素的定义是一个严格的 sequence,子元素必须按一个固定次序出现,顺序错了就是校验失败。

我见过最典型的写法是把<typeAliases>放在<settings>前面,理由很自然——"别名先定义好,后面才好用"。逻辑上没错,但 DTD 不认。还有人把<plugins>挪到最后,图个"插件收尾"的仪式感,同样会红。这类问题的判断方法:把报错点周围的几个标签列出来,对照第 4 节那张顺序表一比,立刻就清楚了。

2.3 第三种:本地缓存了一份"旧版" DTD

这一类最隐蔽。症状是:老标签全部正常,新加的某个标签一写就红,而且报错位置往往指在 DOCTYPE 那一行而不是标签本身。原因是 IDE 的 XML 目录里已经缓存过一份旧版本 DTD,新版本才引入的元素它当然不认识。

判断办法很直接:从工程依赖的 jar 里把真正的 DTD 抠出来,和缓存的对比一下。我在第 3.1 节给了提取命令。还有一种变体情况——团队里有人把 DTD 文件手动放在工程目录里,后来 MyBatis 版本升了,但这份老文件一直没换,于是本地映射指向的是一份过期的定义。这类"半截子映射"比完全没映射更难查,因为看起来什么都配了。

2.4 用命令行做一次"客观验证"

不放心 IDE 的判断,可以用命令行做一次独立验证,绕开所有编辑器缓存。装了 libxml2 的机器上直接跑:

xmllint --noout --dtdvalid ./dtd/mybatis-3-config.dtd src/main/resources/mybatis-config.xml

没有任何输出就代表通过,说明 XML 和 DTD 是匹配的,红叉纯属 IDE 那边的"信息缺失";如果输出里有Element xxx: validity error,那就是真写错了,老老实实按提示改。这个命令还有个好处:它能帮你区分"顺序问题"和"取不到 DTD"这两种外观相同的报错。

3. 把 DTD 请回本地:一次配置长期省心的方案

确认是"取不到"之后,最优解是本地映射,而不是每次打开工程都手动关提示。原因很简单:关校验是关掉了整类检查,以后你真把标签拼错了它也装看不见;本地映射则是用正确的 DTD 替代正确的 DTD,提示能力一点不损失。

3.1 先从 jar 里把 DTD 抠出来

不要去网上随便搜一份,版本对不上反而更麻烦。直接从你工程实际依赖的 jar 里取,保证版本一致:

# 确认实际使用的版本号 mvn dependency:tree | grep mybatis # 从 jar 包里导出 DTD unzip -p ~/.m2/repository/org/mybatis/mybatis/3.5.13/mybatis-3.5.13.jar \ org/apache/ibatis/builder/xml/mybatis-3-config.dtd > mybatis-3-config.dtd unzip -p ~/.m2/repository/org/mybatis/mybatis/3.5.13/mybatis-3.5.13.jar \ org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd > mybatis-3-mapper.dtd

两个文件一起导出来,mapper 的那份顺手也用得上,第 3.4 节会说。导出后建议放到工程目录下统一位置,比如src/main/resources/dtd/或者项目根目录的tools/dtd/。放工程内的好处是:团队其他人拉代码后不用重复操作,路径固定,映射配置也好统一。

提示:把这两个 DTD 一并纳入版本管理。它们只有几 KB,却能让整个团队省掉一轮重复排查,投入产出比很高。

3.2 Eclipse 系的 XML Catalog 映射

在 Eclipse 这一类工具里,映射入口叫 XML Catalog。路径大致是:首选项 → XML → XML Catalog → Add,然后按下面填:

  • Catalog Entry 类型:URI(如果界面里同时有 Public ID 和 URI 两种键,两个都加一份最稳)
  • URI 键:http://mybatis.org/dtd/mybatis-3-config.dtd
  • Location:点 Workspace 选工程里那份 DTD,或者点 File System 选磁盘路径

加完之后,在 Catalog 列表里能看到这条记录。回到编辑器,右键 XML 文件选 Validate,红叉通常立刻消失。如果没消失,先做两步:Project → Clean清一下工程,再看项目属性里的 XML Validator 是否启用。

Public ID 那条可以顺手也加一条,键填-//mybatis.org//DTD Config 3.0//EN,指向同一个文件。有些校验器优先用 Public ID 匹配,两条都配能避免"配了却没生效"的尴尬。

3.3 IDEA 这一类工具的 Schemas and DTDs 映射

入口在:Settings → Languages & Frameworks → Schemas and DTDs。点右上角加号,URI 填http://mybatis.org/dtd/mybatis-3-config.dtd,File 选本地那份 DTD,保存即可。

它还有个更省事的做法:IDEA 本身能从依赖 jar 里自动识别 DTD,如果识别成功,你会看到列表里已经有一条指向 jar 内路径的记录。这种情况下不要重复加 URI 映射,加了反而可能冲突——同一 URI 配了两条不同来源时,优先级不一定是你期望的那条。遇到映射不生效,第一件事就是检查有没有重复条目。

如果工程里 mapper XML 特别多、满屏红,还有个应急开关:同一界面的 Ignored Schemas and DTDs,把对应 URI 加进去。它比彻底关校验温和,只屏蔽这一份 DTD 的检查,其他 XML 约束照常生效。我一般把它当临时过渡手段,不长期用。

3.4 mapper 的 DTD 一起处理掉

mybatis-3-mapper.dtd的报错长得跟 config 一模一样,只是 URI 换了。既然已经把文件取出来了,不如一起映射,两个 URI 各加一条:

http://mybatis.org/dtd/mybatis-3-config.dtd -> dtd/mybatis-3-config.dtd http://mybatis.org/dtd/mybatis-3-mapper.dtd -> dtd/mybatis-3-mapper.dtd

顺便说一句,同类问题在别的框架上也是同一套解法。凡是 XML 里带 DOCTYPE 或 xsi:schemaLocation 指向远程地址的,离线环境下都会红,思路完全一致:把远程资源落地成本地文件,再做 URI 到文件的映射。这套方法我在三四个不同类型的项目里都用过,通用性很强。

4. 元素顺序:DTD 里那条最容易忽略的硬规矩

顺序问题是这条报错里最容易被误判的一类,值得单独拉出来讲清楚,因为它跟网络一点关系都没有。

4.1 标准顺序表,照着排就不会错

mybatis-3-config.dtd对configuration的子元素做了严格次序约束,我把它整理成下面这张表,写配置时对着排一遍就行。带问号的是可选元素,不写没关系,但一旦写就必须在这个位置。

顺序元素是否必填说明
1properties可选外部属性文件引入
2settings可选全局行为开关
3typeAliases可选类型别名
4typeHandlers可选自定义类型处理器
5objectFactory可选对象工厂
6objectWrapperFactory可选包装器工厂
7reflectorFactory可选反射工厂
8plugins可选拦截器插件
9environments可选数据源与环境
10databaseIdProvider可选多数据库标识
11mappers可选映射文件注册

一个可以直接抄的骨架:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="db.properties"/> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="SLF4J"/> </settings> <typeAliases> <package name="com.example.demo.entity"/> </typeAliases> <plugins> <plugin interceptor="com.example.demo.plugin.SlowSqlInterceptor"/> </plugins> <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/UserMapper.xml"/> </mappers> </configuration>

4.2 为什么"调整顺序"能解决问题而"改网络"不能

这两种情况的报错文本高度相似,但含金量完全不同。取不到 DTD 时,校验器是"没法判断",报错挂在 DOCTYPE 那一行;顺序错了则是"判断出来了,就是不合规",报错会精确指向那个越位的标签,并且文本里通常带着The content of element type "configuration" must match这样的字样。

所以看报错位置是个非常有效的分流手段:指在 DOCTYPE 上,走第 3 节;指在具体标签上,先看顺序。我踩过一次坑,顺序问题被我当成网络问题查了半天,最后把 DTD 下载下来打开一看,里面那行 sequence 写得明明白白,纯粹是自己没耐心读。

4.3 一个反直觉的细节:顺序错了也可能"不报错"

还有一种情况值得提醒:不同版本的校验器对"缺少 DTD"时的宽容度不一样。有的工具在取不到 DTD 时干脆跳过校验,于是顺序错了也不提示,等你哪天把 DTD 映射配好了,一堆历史遗留的越位标签突然全红,看着像"配映射把项目搞坏了",其实是把一直存在的问题暴露出来了。

我给团队的建议是:配好本地映射之后,一次性把顺序问题清干净,别留一半红一半不红的状态。不然下一个人接手时,会以为映射方案有副作用,反而把正确做法回退了。

5. 不想动文件也能安静:校验级别与 DOCTYPE 的取舍

有些场景下确实不方便落地 DTD 文件,比如临时连一台别人的机器、或者在一个不能提交额外文件的仓库里工作。这时候有三条退路,我按推荐程度从高到低说。

5.1 只降这一条的级别,别把校验全关

多数 IDE 的 XML 校验支持按规则调级别。以 Eclipse 系为例:首选项 → Validation,找到 XML Validator,再进它的 Settings,把与 DTD 相关的规则从 Error 调成 Warning 或 Ignore。项目级别也可以在项目属性 → Validation 里单独调整,只影响当前项目。

这样做的价值在于保留了大部分检查能力——标签拼错、属性名写错、结构异常仍然会提示,只是"DTD 拿不到"这一条不吵你了。比起把 XML Validator 整个关掉,这个做法安全得多。菜单文字各版本略有出入,找不到就在首选项搜索框里直接搜 XML,基本都能定位。

5.2 删掉 DOCTYPE:能用,但要清楚代价

直接删掉那行<!DOCTYPE configuration ...>,编辑器就没什么可校验的了,红叉自然消失,MyBatis 运行时也照样能解析——因为它的解析逻辑并不依赖 DOCTYPE 存在。这是最快的止血办法。

代价也很明确:失去全部标签补全和结构提示。<settings>里那几十个配置项名字又长又像,少了一层提示,拼错的风险直线上升;更麻烦的是这个错误在运行期才会暴露,而且报错信息往往不指向真正的出错位置。我只在一种情况下会用这招:临时验证"红叉是不是 DOCTYPE 引起的"。验证完就把声明加回去,绝不留在提交里。

5.3 团队协作视角:为什么我坚持"配映射"而不是"关校验"

带过几个人的小团队之后就明白,这类问题真正消耗的不是解决它的十分钟,而是每个人都要重新踩一遍的重复成本。你关掉了自己机器上的校验,别人拉代码还是红的,于是又开一轮讨论。

所以我倾向于把 DTD 文件和映射说明一起进仓库,在 README 或者项目笔记里写清楚三行:文件在哪、URI 填什么、本地路径填什么。新同学照着配一次,之后再也不用管。这件事的成本是一次性的,收益是持续的。

6. 相邻的几个坑:报错解决了,别在这几个地方翻车

DTD 的红叉消掉之后,我经常会顺手把下面几项也检查一遍,因为它们经常和这条报错同时存在,而且更隐蔽。

6.1 BOM 头与编码声明打架

XML 文件保存成带 BOM 的 UTF-8 时,解析器会在<?xml前面看到几个不可见字节,直接抛Content is not allowed in prolog。这个报错和 DTD 报错完全是两码事,但都挂在文件第一行,很容易混淆。

处理办法:把文件编码改成不带 BOM 的 UTF-8,或者用十六进制视图确认开头是不是EF BB BF。命令行下file命令就能看出来,带 BOM 的会标注with BOM。这个坑我在接手别人的配置文件时遇到过不止一次,尤其是从某些编辑器里复制粘贴出来的文件。

6.2 mappers 的路径写法,三种方式别混用

<mappers>下有resource、url、class、package四种写法,含义各不相同,混用会导致"文件明明在,就是找不到"。resource走的是 classpath 相对路径,注意不要带classpath:前缀;url走的是绝对路径或网络地址;class和package走的是接口全限定名。

还有一个隐藏前提:用 Maven 构建时,src/main/java下的 XML 默认不会被打进 classpath。mapper 文件如果和接口放在一起,必须在 pom 里显式配置资源目录包含,否则本地跑得好好的,打包之后就报找不到映射。这个坑跟 DTD 一点关系没有,但排查时经常被混在一起。

6.3 资源过滤把 XML 里的占位符"吃掉"

有些工程会开启资源过滤,让打包时替换掉配置里的占位符。如果过滤规则写得太宽,把 mapper XML 也纳入了,而 mapper 里恰好有${}形式的动态 SQL 片段,就会在构建阶段被误替换,产出物里的 SQL 直接变形。排查方法是把构建产物里的 XML 解出来看一眼,和源码对比。

顺带说,DTD 文件本身也建议排除在过滤之外。虽然它通常不含占位符,但把它放进过滤范围没有好处,反而增加不确定性。

6.4 多模块工程里 DTD 重复,映射指向了错的

多模块项目常见的情况是:父工程和子模块各带一份 DTD,版本还不一致。这时映射到底指向哪一份,取决于你配置的路径,很容易出现"明明映射了还报错"。我的做法是只保留一份,放在一个公共位置,其他地方通过相对路径引用,绝不复制多份。版本升级时也只需要换一个文件。

7. 我个人的处理习惯与几条经验

这几年遇到这条报错,我基本形成了固定动作,很少走弯路。先把工程跑一次确认运行时正常,这一步能立刻排除掉"真写错了"的可能;然后把 DTD 地址粘到浏览器点一下,判断是网络问题还是顺序问题;接着看报错指向哪一行,指向 DOCTYPE 就落本地映射,指向具体标签就查顺序表。整套流程走下来通常不超过十分钟。

有一点体会比较深:这类"编辑器报错、程序正常"的问题,最忌讳的就是动依赖和改版本。我见过太多人第一反应是升级 MyBatis,结果依赖树被搅乱,本来只是红叉的小事变成编译不过的大事。DTD 校验这件事从头到尾都发生在编辑器和 jar 包之间,跟你的业务依赖没有任何关系,动它的收益是零,风险却不小。

另一个小技巧分享给经常在多台机器之间切换的人:把 DTD 文件和一个简短的配置说明放进仓库的一个固定目录,比如docs/dtd/下配一个README.md,写清两条 URI 映射关系和对应的相对路径。换机器时照着点几下就好,不用再回忆"上次是怎么弄的"。我这么干之后,同样的排查在团队里基本绝迹了,偶尔有新人问起,直接把那个目录发过去就行。

还有一个判断经验:如果红叉出现在你刚打开工程的前几秒、刷新一下就消失,那通常是校验器还没走完远程请求,属于加载时序问题,稍微等一下或者手动触发一次 Validate 就好,不用当成故障处理。相反,如果红叉稳定存在、重启也不消失,那才是需要配置映射的信号。这两者的区别,只有多遇到几次才分得清,写在这里给后面的同学省点时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进