Apache Commons Digester 常用 CLI 命令与工具详解

讲清 Apache Commons Digester 没有独立命令行工具的原因,用 java -cp 自建 XML 解析命令行工具,并详解规则 API 与注解式 API 的用法与真实回显。

Apache Commons DigesterApache Commons Digester2026/10/14

Apache Commons Digester 常用 CLI 命令与工具详解

目标:说明 Apache Commons Digester 为什么没有独立命令行工具,并给出一个用 java -cp 直接运行的自建解析工具,同时详解规则 API 与注解式 API 的用法。全部命令与回显已在 Ubuntu 22.04(JDK 17)实测通过。

一、环境信息

本章先准备一个不依赖构建工具的最小运行环境:下载 3.2 的胖包(with-deps.jar 已把传递依赖打进同一个 jar),再写入示例源码,后续所有命令都基于它。

创建目录并下载:

mkdir -p /opt/digester-cli
cd /opt/digester-cli
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 + "}";
    }
}

写入输入文件 servers.xml:

<?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>

💡 技术拓展:把 XML 映射成 Java 对象有多种做法,选型前要先分清各自的内存与用法代价:

方案处理方式内存占用代码量典型场景
SAX事件流式推送,逐节点回调低高(手写回调与状态机)超大 XML、只读遍历
DOM整棵树载入内存后操作高中需随机访问、就地修改结构
StAX拉模式流式,程序主动取事件低中需精确控制读取流程
JAXB注解绑定对象,需 XSD 生成类中低标准 XML ↔ POJO 互转
Digester规则式流式映射,边读边建对象低低配置/规则文件转对象

本文全部示例使用 Apache Commons Digester;它以「规则」描述「解析到哪个路径时做什么」,规则可以用 Java API、注解或 XML 文件三种方式声明,分别在第四、五、六章展开。

二、关于「命令行」:Digester 是库而非 CLI 程序

与 curl、openssl 这类自带命令行的工具不同,Apache Commons Digester 是一个 Java 库,不提供形如 $ digester 这样的可执行程序。实测也印证了这一点:直接尝试运行纯库 jar,会因为没有主类而失败:

curl -fsSL -o commons-digester3-3.2.jar https://repo1.maven.org/maven2/org/apache/commons/commons-digester3/3.2/commons-digester3-3.2.jar
java -jar commons-digester3-3.2.jar

实测回显:

no main manifest attribute, in commons-digester3-3.2.jar

因此,所谓「用 Digester 做命令行」实际是:自己写一个带 main 方法的类,用 Digester 解析命令行传入的 XML,再把结果打印出来,最后用 java -cp 运行它。下一章给出这样一个可直接使用的工具。

三、自建命令行解析工具 XmlModelTool

写入 XmlModelTool.java——一个把 XML 文件路径作为参数、解析后逐条打印的迷你工具:

import org.apache.commons.digester3.Digester;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class XmlModelTool {
    public static void main(String[] args) throws Exception {
        if (args.length < 1) {
            System.err.println("用法: XmlModelTool <xml文件>");
            System.exit(2);
        }
        Digester d = new Digester();
        d.addObjectCreate("servers", ArrayList.class);
        d.addObjectCreate("servers/server", Server.class);
        d.addSetProperties("servers/server");
        d.addSetNext("servers/server", "add");
        List<Server> servers = d.parse(new File(args[0]));
        System.out.println("文件: " + args[0]);
        System.out.println("记录数: " + servers.size());
        for (Server s : servers) { System.out.println(s); }
    }
}

要点:args 为空时打印用法并以退出码 2 结束(便于脚本判断);parse(new File(args[0])) 用 File 对象而非字符串,避免把路径当 URL 解析。

编译并运行:

cd /opt/digester-cli
mkdir -p out
javac -encoding UTF-8 -cp commons-digester3-3.2-with-deps.jar -d out Server.java XmlModelTool.java
java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" XmlModelTool servers.xml

实测回显:

文件: servers.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}

不带参数运行,验证用法提示与退出码:

java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" XmlModelTool
echo "exit=$?"

实测回显:

用法: XmlModelTool <xml文件>
exit=2

这个工具就是「Digester 的命令行形态」:java -cp <类路径> XmlModelTool <xml文件>。只要替换 Server 与规则,就能复用成解析任意结构 XML 的 CLI。

四、规则 API 方法参考

Digester 的规则 API 都挂在 Digester 实例上,方法名一律以 add 开头、第一个参数是元素路径 pattern(用 / 连接,如 servers/server)。写入 RuleApiDemo.java 逐方法演示:

import org.apache.commons.digester3.Digester;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

public class RuleApiDemo {
    public static void main(String[] args) throws Exception {
        Digester d1 = new Digester();
        d1.addObjectCreate("servers", ArrayList.class);
        d1.addObjectCreate("servers/server", Server.class);
        d1.addSetProperties("servers/server");
        d1.addSetNext("servers/server", "add");
        List<Server> r1 = d1.parse(new StringReader(
            "<servers><server name=\"a\" host=\"h1\" port=\"1\"/></servers>"));
        System.out.println("[addObjectCreate/addSetProperties/addSetNext] " + r1);

        Digester d2 = new Digester();
        d2.addObjectCreate("servers", ArrayList.class);
        d2.addObjectCreate("servers/server", Server.class);
        d2.addSetProperties("servers/server");
        d2.addBeanPropertySetter("servers/server/host");
        d2.addSetNext("servers/server", "add");
        List<Server> r2 = d2.parse(new StringReader(
            "<servers><server name=\"b\" host=\"attrHost\" port=\"2\"><host>bodyHost</host></server></servers>"));
        System.out.println("[addBeanPropertySetter] host=" + r2.get(0).getHost());

        Digester d3 = new Digester();
        d3.addObjectCreate("servers", ArrayList.class);
        d3.addObjectCreate("servers/server", Server.class);
        d3.addCallMethod("servers/server/name", "setName", 0);
        d3.addSetNext("servers/server", "add");
        List<Server> r3 = d3.parse(new StringReader(
            "<servers><server><name>fromBodyCall</name></server></servers>"));
        System.out.println("[addCallMethod] name=" + r3.get(0).getName());

        Digester d4 = new Digester();
        d4.addObjectCreate("servers", ArrayList.class);
        d4.addObjectCreate("servers/server", Server.class);
        d4.addSetProperty("servers/server/property", "name", "value");
        d4.addSetNext("servers/server", "add");
        List<Server> r4 = d4.parse(new StringReader(
            "<servers><server><property name=\"host\" value=\"forced.example.com\"/></server></servers>"));
        System.out.println("[addSetProperty] host=" + r4.get(0).getHost());
    }
}

编译运行:

cd /opt/digester-cli
javac -encoding UTF-8 -cp commons-digester3-3.2-with-deps.jar -d out Server.java RuleApiDemo.java
java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" RuleApiDemo

实测回显:

[addObjectCreate/addSetProperties/addSetNext] [Server{name=a, host=h1, port=1}]
[addBeanPropertySetter] host=bodyHost
[addCallMethod] name=fromBodyCall
[addSetProperty] host=forced.example.com

四行输出对应四组规则的实测结果,逐条解读:

  1. 第一行:addObjectCreate(pattern, Class) 创建对象、addSetProperties(pattern) 按属性名灌值、addSetNext(pattern, "add") 挂到父列表,三者组合即「XML 属性 → 对象列表」的常规链路。
  2. 第二行:addBeanPropertySetter("servers/server/host") 用子元素 <host>bodyHost</host> 的元素体设置 Server.host 属性;它覆盖了属性方式传入的 attrHost,结果为 bodyHost。
  3. 第三行:addCallMethod("servers/server/name", "setName", 0) 调用 setName。第三个参数 paramCount=0 的含义是「用元素体自动转成 1 个 String 参数」,因此把 <name>fromBodyCall</name> 的元素体传了进去。
  4. 第四行:addSetProperty("servers/server/property", "name", "value") 用子元素 property 的 name/value 属性对,指定「属性名 → 值」直接注入,把 host 设成了 forced.example.com。

常用规则 API 对照:

规则 API触发时机典型用途
addObjectCreate(pattern, Class)进入元素时创建对象并压栈
addSetProperties(pattern)进入元素时按元素属性名匹配同名 setter
addSetProperty(pattern, attrName, attrValue)进入元素时按指定属性直接设值
addBeanPropertySetter(pattern)进入元素时用元素体设置同名 Bean 属性
addCallMethod(pattern, method, paramCount)进入元素时调方法,元素体作参数
addCallParam(pattern, paramIndex)进入元素时为方法调用提供指定参数(常配 addCallMethod)
addSetNext(pattern, method)离开元素时把对象挂到父对象(如 add)
addSetTop / addSetRoot进入/离开时压入栈顶 / 弹到根
addFactoryCreate(pattern, factory)进入元素时用自定义工厂创建对象

其中 paramCount=0 表示「元素体作 1 个 String 参数」是最易踩的语义坑;若要 0 个参数真正无参调用,应使用 addCallMethod(pattern, method) 的两参重载。

五、注解式 API

除 Java 规则 API 与 XML 规则文件外,3.x 还支持注解式声明:把规则直接标在 POJO 类/字段/方法上,用 FromAnnotationsRuleModule 扫描绑定。它让「对象结构」与「映射规则」集中在一处,维护更直观。

写入带注解的 POJO AnnotatedServer.java:

import org.apache.commons.digester3.annotations.rules.ObjectCreate;
import org.apache.commons.digester3.annotations.rules.SetProperty;

@ObjectCreate(pattern = "servers/server")
public class AnnotatedServer {
    @SetProperty(pattern = "servers/server", attributeName = "name")
    private String name;
    @SetProperty(pattern = "servers/server", attributeName = "host")
    private String host;
    @SetProperty(pattern = "servers/server", attributeName = "port")
    private Integer 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 Integer getPort() { return port; }
    public void setPort(Integer port) { this.port = port; }

    @Override
    public String toString() {
        return "AnnotatedServer{name=" + name + ", host=" + host + ", port=" + port + "}";
    }
}

写入承载列表的汇集类 ServerCatalog.java(用 @SetNext 把子对象收集进列表):

import org.apache.commons.digester3.annotations.rules.ObjectCreate;
import org.apache.commons.digester3.annotations.rules.SetNext;
import java.util.ArrayList;
import java.util.List;

@ObjectCreate(pattern = "servers")
public class ServerCatalog {
    private final List<AnnotatedServer> servers = new ArrayList<>();

    @SetNext
    public void addServer(AnnotatedServer server) {
        servers.add(server);
    }

    public List<AnnotatedServer> getServers() { return servers; }
}

写入加载程序 AnnotationDemo.java(FromAnnotationsRuleModule.configureRules() 里绑定要扫描的类):

import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.annotations.FromAnnotationsRuleModule;
import org.apache.commons.digester3.binder.DigesterLoader;
import java.io.File;

public class AnnotationDemo {
    public static void main(String[] args) throws Exception {
        DigesterLoader loader = DigesterLoader.newLoader(new FromAnnotationsRuleModule() {
            @Override
            protected void configureRules() {
                bindRulesFrom(ServerCatalog.class);
                bindRulesFrom(AnnotatedServer.class);
            }
        });
        Digester d = loader.newDigester();
        ServerCatalog catalog = d.parse(new File("servers.xml"));
        System.out.println("注解式解析到 " + catalog.getServers().size() + " 台:");
        for (AnnotatedServer s : catalog.getServers()) { System.out.println("  " + s); }
    }
}

编译运行:

cd /opt/digester-cli
javac -encoding UTF-8 -cp commons-digester3-3.2-with-deps.jar -d out Server.java AnnotatedServer.java ServerCatalog.java AnnotationDemo.java
java -Dfile.encoding=UTF-8 -cp "commons-digester3-3.2-with-deps.jar:out" AnnotationDemo

实测回显:

注解式解析到 3 台:
  AnnotatedServer{name=web01, host=10.0.0.11, port=8080}
  AnnotatedServer{name=web02, host=10.0.0.12, port=8080}
  AnnotatedServer{name=db01, host=10.0.0.21, port=3306}

注解与规则的对应关系:

注解等价规则 API标注位置作用
@ObjectCreateaddObjectCreate类指定 pattern 上创建该类的对象
@SetPropertyaddSetProperty字段用元素属性设值(须给 attributeName)
@BeanPropertySetteraddBeanPropertySetter字段/方法用元素体设值
@CallMethodaddCallMethod方法解析到 pattern 时调用该方法
@SetNextaddSetNext方法把父/子对象关联起来(本示例收集子对象)
@SetRoot / @SetTopaddSetRoot / addSetTop字段/方法关联根/栈顶对象

需要特别说明:Digester 3.2 的注解集合里没有 @SetProperties(对应整组属性注入的那个注解)。需要「按属性名批量注入」时,要么用 Java 规则 API 的 addSetProperties,要么在 POJO 上逐个字段标 @SetProperty。此外 @SetNext 也对应 XML 规则里的 set-next-rule,本示例用它把每个 AnnotatedServer 收进 ServerCatalog 的列表。

六、XML 规则文件加载(声明规则的第三种方式)

规则也可以写在 XML 文件里,用 FromXmlRulesModule 加载(详见《Apache Commons Digester 配置参数详解》第六章的规则文件结构)。最小用法如下:写入 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>

写入 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); }
    }
}

编译运行:

cd /opt/digester-cli
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}

三种声明方式(Java API / 注解 / XML 规则文件)产出的对象完全一致,可按团队习惯选择:代码内聚用 Java API,对象即配置用注解,规则需外部化(可运行时替换)用 XML 文件。注意 classname="Server" 用的是类全名,示例因 Server 在默认包而直接写 Server;若放在 net.quzuo.demo 包下则应写 net.quzuo.demo.Server。

七、参考链接

  1. Apache Commons Digester 官方主页:https://commons.apache.org/proper/commons-digester/
  2. Digester 3.2 API 文档(规则 API 与注解清单):https://commons.apache.org/proper/commons-digester/apidocs/index.html
  3. 注解式 API 所在包 org.apache.commons.digester3.annotations:https://commons.apache.org/proper/commons-digester/apidocs/org/apache/commons/digester3/annotations/package-summary.html
  4. XML 规则文件 DTD:https://commons.apache.org/proper/commons-digester/dtds/digester-rules-3.0.dtd
  5. Maven 中央仓库 3.2 目录(含 with-deps.jar):https://repo1.maven.org/maven2/org/apache/commons/commons-digester3/3.2/
  6. SAX 规范(Digester 底层解析基础):https://docs.oracle.com/en/java/javase/17/docs/api/java.xml/org/xml/sax/package-summary.html