Apache Commons Digester 配置参数详解
详解 Apache Commons Digester 的核心配置参数:命名空间感知、DTD 校验、SAX 特性与属性、错误处理、类加载器、规则注册顺序以及 digester-rules.xml 规则文件结构。
Apache Commons Digester 配置参数详解
目标:逐一讲清 Apache Commons Digester 的配置参数与规则注册语义,并给出每一项的真实现象与回显。全部命令与回显已在 Ubuntu 22.04(JDK 17)实测通过。
一、环境信息
本章先准备一个可直接运行的最小环境,后续每一节的示例都基于它。Commons Digester 运行时不必依赖构建工具,把「主 jar + 传递依赖」这一个胖包下载下来,即可用 java -cp 直接编译运行。
先创建目录并下载 3.2 的胖包(with-deps.jar 已把 cglib、commons-beanutils、commons-logging 打进同一个 jar):
mkdir -p /opt/digester-cfg
cd /opt/digester-cfg
curl -fsSLO https://repo1.maven.org/maven2/org/apache/commons/commons-digester3/3.2/commons-digester3-3.2-with-deps.jar
ls -l
实测回显:
-rw-r--r-- 1 root root 734212 Oct 13 16:57 commons-digester3-3.2-with-deps.jar
写入被映射的 POJO Server.java:
public class Server {
private String name;
private String host;
private int port;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
@Override
public String toString() {
return "Server{name=" + name + ", host=" + host + ", port=" + port + "}";
}
}
写入配置文件 gservers.xml(用于规则顺序与元素体传参的示例):
<?xml version="1.0" encoding="UTF-8"?>
<servers>
<server name="web01" host="ATTR">BODYHOST</server>
</servers>
写入本章的综合演示程序 ConfigDemo.java(把默认值与各配置项现象一次性打印出来,后面每章引用其中一段):
import org.apache.commons.digester3.Digester;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
public class ConfigDemo {
static Digester serverDigester() {
Digester d = new Digester();
d.addObjectCreate("servers", ArrayList.class);
d.addObjectCreate("servers/server", Server.class);
d.addSetProperties("servers/server");
d.addSetNext("servers/server", "add");
return d;
}
static final String PLAIN =
"<servers>"
+ "<server name=\"web01\" host=\"10.0.0.11\" port=\"8080\"/>"
+ "<server name=\"web02\" host=\"10.0.0.12\" port=\"8080\"/>"
+ "<server name=\"db01\" host=\"10.0.0.21\" port=\"3306\"/>"
+ "</servers>";
static final String NS =
"<ns:servers xmlns:ns=\"urn:demo\">"
+ "<ns:server name=\"web01\" host=\"10.0.0.11\" port=\"8080\"/>"
+ "<ns:server name=\"web02\" host=\"10.0.0.12\" port=\"8080\"/>"
+ "</ns:servers>";
static final String DTD =
"<!DOCTYPE servers ["
+ "<!ELEMENT servers (server*)>"
+ "<!ELEMENT server EMPTY>"
+ "<!ATTLIST server name CDATA #IMPLIED host CDATA #IMPLIED port CDATA #IMPLIED>"
+ "]>"
+ PLAIN;
public static void main(String[] args) throws Exception {
Digester d0 = new Digester();
System.out.println("默认 namespaceAware = " + d0.getNamespaceAware());
System.out.println("默认 validating = " + d0.getValidating());
System.out.println("默认 classLoader = " + d0.getClassLoader());
System.out.println("default errorHandler= " + d0.getErrorHandler());
System.out.println("feature(disallow-doctype-decl) = "
+ d0.getFeature("http://apache.org/xml/features/disallow-doctype-decl"));
System.out.println("--- A. setNamespaceAware(false) + 普通 XML ---");
Digester da = serverDigester();
da.setNamespaceAware(false);
List<Server> ra = da.parse(new StringReader(PLAIN));
System.out.println("解析到 " + ra.size() + " 台");
System.out.println("--- B. setNamespaceAware(true) + 命名空间 XML ---");
Digester db = new Digester();
db.setNamespaceAware(true);
db.addObjectCreate("servers", ArrayList.class);
db.addObjectCreate("servers/server", Server.class);
db.addSetProperties("servers/server");
db.addSetNext("servers/server", "add");
List<Server> rb = db.parse(new StringReader(NS));
System.out.println("命名空间模式解析到 " + rb.size() + " 台: " + rb);
System.out.println("--- C. 默认(未开命名空间) + 命名空间 XML ---");
Digester dc = serverDigester();
dc.setNamespaceAware(false);
List<Server> rc = dc.parse(new StringReader(NS));
System.out.println("未开命名空间解析到 " + (rc == null ? 0 : rc.size()) + " 台");
System.out.println("--- D. setValidating(true) + 内嵌 DTD ---");
Digester dd = serverDigester();
dd.setValidating(true);
dd.setErrorHandler(new CapturingHandler());
List<Server> rd = dd.parse(new StringReader(DTD));
System.out.println("DTD 校验通过,解析到 " + rd.size() + " 台");
System.out.println("--- E. setFeature(disallow-doctype-decl,true) ---");
Digester de = serverDigester();
de.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
try {
de.parse(new StringReader(DTD));
System.out.println("未按预期拒绝");
} catch (Exception ex) {
System.out.println("已拒绝 DOCTYPE: " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
}
System.out.println("--- F. setErrorHandler 捕获格式错误 ---");
Digester df = serverDigester();
CapturingHandler h = new CapturingHandler();
df.setErrorHandler(h);
try {
df.parse(new StringReader("<servers><server name=\"x\"></servers>"));
} catch (Exception ex) {
System.out.println("抛出: " + ex.getClass().getSimpleName());
}
System.out.println("errorHandler 记录条数: " + h.messages.size());
if (!h.messages.isEmpty()) {
System.out.println("首条: " + h.messages.get(0));
}
System.out.println("--- G. 规则注册顺序(先 SetProperties 设 host,后 setHost 用元素体覆盖) ---");
Digester dg = new Digester();
dg.addObjectCreate("servers", ArrayList.class);
dg.addObjectCreate("servers/server", Server.class);
dg.addSetProperties("servers/server");
dg.addCallMethod("servers/server", "setHost", 0);
dg.addSetNext("servers/server", "add");
List<Server> rg = dg.parse(new File("gservers.xml"));
System.out.println("首台 host = " + rg.get(0).getHost());
}
static class CapturingHandler implements ErrorHandler {
final List<String> messages = new ArrayList<>();
public void warning(SAXParseException e) { messages.add("WARN " + e.getMessage()); }
public void error(SAXParseException e) { messages.add("ERROR " + e.getMessage()); }
public void fatalError(SAXParseException e) { messages.add("FATAL " + e.getMessage()); }
}
}
编译并运行:
cd /opt/digester-cfg
mkdir -p out
javac -encoding UTF-8 -cp commons-digester3-3.2-with-deps.jar -d out Server.java ConfigDemo.java
java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" ConfigDemo
实测回显:
默认 namespaceAware = false
默认 validating = false
默认 classLoader = jdk.internal.loader.ClassLoaders$AppClassLoader@30946e09
default errorHandler= null
feature(disallow-doctype-decl) = false
--- A. setNamespaceAware(false) + 普通 XML ---
解析到 3 台
--- B. setNamespaceAware(true) + 命名空间 XML ---
命名空间模式解析到 2 台: [Server{name=web01, host=10.0.0.11, port=8080}, Server{name=web02, host=10.0.0.12, port=8080}]
--- C. 默认(未开命名空间) + 命名空间 XML ---
未开命名空间解析到 0 台
--- D. setValidating(true) + 内嵌 DTD ---
DTD 校验通过,解析到 3 台
--- E. setFeature(disallow-doctype-decl,true) ---
已拒绝 DOCTYPE: SAXParseException: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.
--- F. setErrorHandler 捕获格式错误 ---
抛出: SAXParseException
errorHandler 记录条数: 1
首条: FATAL The end-tag for element type "server" must end with a '>' delimiter.
--- G. 规则注册顺序(先 SetProperties 设 host,后 setHost 用元素体覆盖) ---
首台 host = BODYHOST
说明:上表为程序的标准输出(stdout)。E、F 两段让解析抛错时,Digester 会把默认的 fatalError 日志堆栈写到 stderr(以 SEVERE 开头),属正常现象,不影响 stdout 结果。程序首次运行还会打印 log4j:WARN No appenders could be found 的提示,同样可忽略。
💡 技术拓展:把 XML 映射成 Java 对象有多种做法,配置方式与内存代价各不相同:
| 方案 | 处理方式 | 内存占用 | 代码量 | 典型场景 |
|---|---|---|---|---|
| SAX | 事件流式推送,逐节点回调 | 低 | 高(手写回调与状态机) | 超大 XML、只读遍历 |
| DOM | 整棵树载入内存后操作 | 高 | 中 | 需随机访问、就地修改结构 |
| StAX | 拉模式流式,程序主动取事件 | 低 | 中 | 需精确控制读取流程 |
| JAXB | 注解绑定对象,需 XSD 生成类 | 中 | 低 | 标准 XML ↔ POJO 互转 |
| Digester | 规则式流式映射,边读边建对象 | 低 | 低 | 配置/规则文件转对象 |
本文全部示例使用 Apache Commons Digester 的「规则式流式映射」;它底层仍是 SAX 解析器,因此第三、四、五章讲到的参数,本质是对底层 SAX/XMLReader 的配置。
二、配置参数总览
Digester 把配置分成三组:一组开关解析模式(是否命名空间感知、是否校验),一组透传给底层 SAX 解析器(feature 与 property),还有一组是 Digester 自身的行为(错误处理、类加载器)。下面是本文覆盖的核心 setter/getter 与实测默认值(默认值取自上一章程序输出):
| 配置项 | Setter / Getter | 实测默认值 | 作用 |
|---|---|---|---|
| 命名空间感知 | setNamespaceAware / getNamespaceAware | false | 是否按命名空间解析,开启后规则 pattern 用 localName 匹配 |
| 文档校验 | setValidating / getValidating | false | 是否按 DTD/XSD 校验文档结构 |
| 解析器特性 | setFeature / getFeature | 按解析器默认(示例 disallow-doctype-decl 为 false) | 透传 SAX 解析器 feature,如禁用 DOCTYPE |
| 解析器属性 | setProperty / getProperty | 按解析器默认 | 透传 SAX 解析器 property,如外部实体访问策略 |
| 错误处理器 | setErrorHandler / getErrorHandler | null | 自定义 warning/error/fatalError 回调 |
| 类加载器 | setClassLoader / getClassLoader | 应用类加载器(AppClassLoader) | 规则创建对象时用于解析类名 |
| XInclude | setXIncludeAware / getXIncludeAware | false | 是否解析 XML Inclusions |
| 实体解析器 | setEntityResolver / getEntityResolver | null | 自定义外部实体定位 |
| 公共标识 | setPublicId / getPublicId | null | 设置 DTD 的 publicId |
要点:所有 setter 都应在第一次 parse() 之前调用,配置只在构造 XMLReader 时生效;已开始解析后改这些参数不会改变行为。
三、命名空间感知:setNamespaceAware
本节对应上一章输出中的 A、B、C 三段。默认 false,此时规则 pattern 用元素的 qName(含前缀)匹配;true 时用 localName(剥离前缀)匹配。
A 段(setNamespaceAware(false) + 普通无命名空间 XML)——pattern servers/servers/server 与 qName 一致,正常解析:
--- A. setNamespaceAware(false) + 普通 XML ---
解析到 3 台
B 段(setNamespaceAware(true) + 带命名空间 XML)——XML 里是 <ns:servers xmlns:ns="urn:demo">,但规则仍写 servers/servers/server,因为命名空间感知下匹配用的是被剥离前缀后的 localName:
--- B. setNamespaceAware(true) + 命名空间 XML ---
命名空间模式解析到 2 台: [Server{name=web01, host=10.0.0.11, port=8080}, Server{name=web02, host=10.0.0.12, port=8080}]
注意这里解析到的是 2 台(该段 XML 只写了 web01、web02 两条),且能正确生成对象。
C 段(默认 setNamespaceAware(false) + 同一份命名空间 XML)——此时匹配用 qName ns:servers,与规则里的 servers 不相等,因此一条都匹配不到:
--- C. 默认(未开命名空间) + 命名空间 XML ---
未开命名空间解析到 0 台
结论:XML 带命名空间时,必须 setNamespaceAware(true),并让 pattern 只写 localName(不要带前缀);否则规则静默失配,parse() 返回空(示例中 null,代码里用 rc == null ? 0 : rc.size() 做了空值保护)。这条差异是 Digester 使用中最常见的「解析不到对象」根因。
四、文档校验:setValidating
本节对应输出中的 D 段。默认 false,即不按 DTD/XSD 校验文档,仅做基本的格式良好(well-formed)检查;setValidating(true) 后,解析器会要求文档带 DTD(或 Schema)并对结构、属性做校验。
D 段用一份内嵌 DTD 的 XML(在 <!DOCTYPE servers 后以内嵌子集定义 server 必须满足的属性)演示开启校验后仍能正常解析:
--- D. setValidating(true) + 内嵌 DTD ---
DTD 校验通过,解析到 3 台
要点与注意:
setValidating(true)时必须设置错误处理器(见第六章),否则校验错误只会走默认日志、不易定位。- 校验失败会以
SAXParseException抛出,可利用它做「配置合法性」的前置检查。 - 校验是 Digester 透传给底层 SAX 解析器的能力;它不改规则行为,只增加结构约束。若 XML 没有 DTD/Schema,
setValidating(true)会因找不到校验依据而报错,此时应保持默认false。
五、解析器特性与属性:setFeature / setProperty
本节对应输出中的 E 段。setFeature(name, value) 与 setProperty(name, value) 把配置透传给底层 SAX 解析器(其底层是 XMLReader)。
E 段演示一个安全加固场景:设置 disallow-doctype-decl 特性为 true,让解析器在遇到 DOCTYPE 时直接拒绝,从而防御 XXE(XML External Entity)注入:
--- E. setFeature(disallow-doctype-decl,true) ---
已拒绝 DOCTYPE: SAXParseException: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.
实测确认该特性默认值为 false(见第二章总览输出),需显式开启。常用特性与属性对照如下:
| 名称 | 类型 | 实测默认 | 推荐值 | 作用 |
|---|---|---|---|---|
http://apache.org/xml/features/disallow-doctype-decl | feature | false | true(不需 DTD 时) | 禁止 DOCTYPE,阻断 XXE |
http://xml.org/sax/features/external-general-entities | feature | 多为 true | false | 禁止外部通用实体 |
http://xml.org/sax/features/external-parameter-entities | feature | 多为 true | false | 禁止外部参数实体 |
http://apache.org/xml/features/nonvalidating/load-external-dtd | feature | 多为 true | false | 不加载外部 DTD |
http://javax.xml.XMLConstants/property/accessExternalDTD | property | 依实现 | "" | 禁止访问外部 DTD |
http://javax.xml.XMLConstants/property/accessExternalSchema | property | 依实现 | "" | 禁止访问外部 Schema |
用法与 setFeature 相同,属性用 setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "") 这类调用。注意:这些名字是否被接受取决于底层解析器(JDK 自带的 Xerces 支持上面这些),不支持的解析器会抛 SAXNotRecognizedException。
⚠️ 安全提示:默认解析器允许加载外部实体,仅在可信输入源下使用默认配置;面向不可信输入时应按上表开启
disallow-doctype-decl等加固项。
六、错误处理:setErrorHandler
本节对应输出中的 F 段。getErrorHandler() 实测默认返回 null,此时 Digester 用内部的默认处理器(把错误经 commons-logging 记日志)。要拿到结构化的错误信息,需实现 org.xml.sax.ErrorHandler 并调用 setErrorHandler。
F 段用一份格式错误的 XML(<server name="x"></servers> 开始/结束标签不匹配)演示捕获过程:
--- F. setErrorHandler 捕获格式错误 ---
抛出: SAXParseException
errorHandler 记录条数: 1
首条: FATAL The end-tag for element type "server" must end with a '>' delimiter.
解读:parse() 最终仍以 SAXParseException 抛出(说明错误已发生),但自定义处理器在抛出前被回调一次,把它归为 fatalError,因此记录条数为 1、首条以 FATAL 开头。ErrorHandler 接口有三个方法:
void warning(SAXParseException e) throws SAXException; // 非致命告警
void error(SAXParseException e) throws SAXException; // 可恢复错误(校验失败常走这里)
void fatalError(SAXParseException e) throws SAXException; // 不可恢复错误(格式错误走这里)
实践中常配合 setValidating(true) 一起用:校验错误走 error,格式错误走 fatalError,可在处理器里分类收集,形成可读的校验报告。
七、类加载器:setClassLoader
本节对应输出中的第一组默认值。getClassLoader() 实测默认返回应用类加载器(示例输出 jdk.internal.loader.ClassLoaders$AppClassLoader@30946e09)。Digester 在按规则创建对象、解析属性类型时会用它去 Class.forName,因此当目标类不在应用类路径、而由自定义加载器提供时,需要显式设置。
典型用法(把规则文件所在上下文的类加载器交给 Digester):
import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.binder.DigesterLoader;
import org.apache.commons.digester3.xmlrules.FromXmlRulesModule;
import java.io.File;
public class CustomLoaderDemo {
public static void main(String[] args) throws Exception {
DigesterLoader loader = DigesterLoader.newLoader(new FromXmlRulesModule() {
@Override
protected void loadRules() {
loadXMLRules(new File("digester-rules.xml"));
}
});
Digester d = loader.newDigester();
d.setClassLoader(Thread.currentThread().getContextClassLoader());
System.out.println("已设置类加载器: " + d.getClassLoader());
}
}
要点:setClassLoader 只影响 Digester 解析类名的来源,不改规则匹配;在聚合了多个业务插件(各插件类由独立加载器提供)的场景里,把它设为能同时看到这些类的加载器即可。若目标类都在应用类路径内,保持默认即可,无需设置。
八、规则注册与执行顺序
本节对应输出中的 G 段。Digester 的核心是「规则(Rule)」:每个 pattern 上注册一组规则,进入元素时按注册顺序依次触发。因此同一属性被多条规则写入时,后注册的规则生效。
G 段在同一 pattern servers/server 上先注册 addSetProperties(把属性 host="ATTR" 灌入 host 字段),随后注册 addCallMethod("servers/server", "setHost", 0)(用元素体 BODYHOST 调 setHost)。由于后者注册更晚,它覆盖了前者:
--- G. 规则注册顺序(先 SetProperties 设 host,后 setHost 用元素体覆盖) ---
首台 host = BODYHOST
注意 addCallMethod(pattern, methodName, paramCount) 的第三个参数:传 0 表示「用元素体自动转成 1 个 String 参数调用」(而非「0 个参数调用」),这一点极易误解——所以此行才把 <server> 的元素体文本 BODYHOST 传进了 setHost。规则顺序的三条实践结论:
- 同一 pattern 的多条规则按注册先后触发,写同一目标时后者胜出;
addObjectCreate要最先注册(先有对象,后续 set/call 才有目标);addSetNext通常在末尾注册(对象填充完成后再挂到父对象)。
九、XML 规则文件 digester-rules.xml
把规则从 Java 代码搬到 XML 文件,可让规则与代码解耦。规则文件的根元素是 <digester-rules>,内部用 <pattern value="servers"> 这类元素声明匹配路径,每个 pattern 下挂具体规则元素。写入 digester-rules.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE digester-rules PUBLIC "-//Apache Commons //DTD digester-rules XML V1.0//EN"
"http://commons.apache.org/digester/dtds/digester-rules-3.0.dtd">
<digester-rules>
<pattern value="servers">
<object-create-rule classname="java.util.ArrayList"/>
</pattern>
<pattern value="servers/server">
<object-create-rule classname="Server"/>
<set-properties-rule/>
<set-next-rule methodname="add"/>
</pattern>
</digester-rules>
逐项说明:<pattern value="servers"> 对应 Java 里的 addObjectCreate("servers", ArrayList.class) 等调用;<object-create-rule> 等价于 addObjectCreate(pattern, Class);<set-properties-rule/> 等价于 addSetProperties(pattern);<set-next-rule methodname="add"/> 等价于 addSetNext(pattern, "add")。写入输入文件 servers.xml(含三条 server 记录):
<?xml version="1.0" encoding="UTF-8"?>
<servers>
<server name="web01" host="10.0.0.11" port="8080"/>
<server name="web02" host="10.0.0.12" port="8080"/>
<server name="db01" host="10.0.0.21" port="3306"/>
</servers>
通过 FromXmlRulesModule 加载该文件并解析,写入 XmlRulesDemo.java:
import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.binder.DigesterLoader;
import org.apache.commons.digester3.xmlrules.FromXmlRulesModule;
import java.io.File;
import java.util.List;
public class XmlRulesDemo {
public static void main(String[] args) throws Exception {
DigesterLoader loader = DigesterLoader.newLoader(new FromXmlRulesModule() {
@Override
protected void loadRules() {
loadXMLRules(new File("digester-rules.xml"));
}
});
Digester d = loader.newDigester();
List<Server> servers = d.parse(new File("servers.xml"));
System.out.println("XML 规则文件加载成功,解析到 " + servers.size() + " 台:");
for (Server s : servers) { System.out.println(" " + s); }
}
}
编译运行(servers.xml 内容为三条 server 记录):
cd /opt/digester-cfg
javac -encoding UTF-8 -cp commons-digester3-3.2-with-deps.jar -d out Server.java XmlRulesDemo.java
java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" XmlRulesDemo
实测回显:
XML 规则文件加载成功,解析到 3 台:
Server{name=web01, host=10.0.0.11, port=8080}
Server{name=web02, host=10.0.0.12, port=8080}
Server{name=db01, host=10.0.0.21, port=3306}
规则文件可用的元素集合(对应各 addXxx 规则 API)如下:
| 规则元素 | 对应 API | 作用 |
|---|---|---|
object-create-rule | addObjectCreate | 进入 pattern 时创建对象 |
set-properties-rule | addSetProperties | 按属性名匹配 setter 灌值 |
set-property-rule | addSetProperty | 用子元素 name/value 指定属性 |
set-next-rule | addSetNext | 把当前对象挂到父对象 |
call-method-rule | addCallMethod | 调用方法,可带子元素参数 |
call-param-rule / object-param-rule | addCallParam / addObjectParam | 为方法调用提供参数 |
node-create-rule | addNodeCreate | 创建 DOM Node |
factory-create-rule | addFactoryCreate | 用自定义工厂创建对象 |
bean-property-setter-rule | addBeanPropertySetter | 用元素体设置同名 Bean 属性 |
set-nested-properties-rule | addSetNestedProperties | 设置嵌套/内层属性 |
set-top-rule / set-root-rule | addSetTop / addSetRoot | 把对象压入/弹出栈顶或根 |
<pattern> 还支持 include 引入其他规则文件、alias 定义别名,便于大项目的规则复用。要点:规则文件名与路径以代码里 loadXMLRules(new File("digester-rules.xml")) 传入的为准,DTD 仅是文档约束、不参与运行时校验。
十、参考链接
- Apache Commons Digester 官方主页:https://commons.apache.org/proper/commons-digester/
- Digester 3.2 API 文档(含 setFeature/setProperty 等签名):https://commons.apache.org/proper/commons-digester/apidocs/index.html
- digester-rules DTD(规则文件结构定义):https://commons.apache.org/proper/commons-digester/dtds/digester-rules-3.0.dtd
- Maven 中央仓库 3.2 目录(含 with-deps.jar):https://repo1.maven.org/maven2/org/apache/commons/commons-digester3/3.2/
- SAX ErrorHandler 规范:https://docs.oracle.com/en/java/javase/17/docs/api/java.xml/org/xml/sax/ErrorHandler.html
- XML 解析安全(XXE 与外部实体):https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- JAXP 特性与属性常量(disallow-doctype-decl、accessExternalDTD 等):https://docs.oracle.com/en/java/javase/17/docs/api/java.xml/javax/xml/XMLConstants.html
- Apache Commons BeanUtils 文档(属性注入底层实现):https://commons.apache.org/proper/commons-beanutils/