Apache Commons Configuration 常用 CLI 命令与工具详解
以 jshell 与 javac/java -cp 实操 Apache Commons Configuration 2.15.1:properties/XML/INI/YAML/JSON 最小示例、jar 清单与典型报错,回显全部实测。
Apache Commons Configuration 常用 CLI 命令与工具详解
目标:在没有 IDE、不搭工程的情况下,用 JDK 自带命令行工具(jshell、javac/java -cp、jar、javap)直接加载并验证 Apache Commons Configuration 2.15.1 的各类配置源,给出每个数据源所需 jar 清单、最小示例与实测输出,已在 Ubuntu 22.04(OpenJDK 17.0.20.1)实测通过。
一、环境信息
本篇所有命令与回显均在以下环境实测采集:
- 操作系统:Ubuntu 22.04,x86_64
- JDK:OpenJDK 17.0.20.1(含 jshell 17.0.20.1)
- 组件版本:org.apache.commons:commons-configuration2:2.15.1
- jar 目录:
/opt/commons-config/lib/(全部依赖),/opt/commons-config/lib-core/(仅必选依赖) - 工作目录:
/opt/commons-config/demos(源码与编译输出) - 执行用户:root
jar 清单(ls -l /opt/commons-config/lib/,实测):
total 4896
-rw------- 1 root root 689129 Oct 13 08:00 commons-configuration2-2.15.1.jar
-rw-r--r-- 1 root root 609182 Oct 13 08:00 commons-io-2.22.0.jar
-rw-r--r-- 1 root root 713862 Oct 13 08:00 commons-lang3-3.20.0.jar
-rw-r--r-- 1 root root 74010 Oct 13 08:00 commons-logging-1.3.6.jar
-rw-r--r-- 1 root root 264873 Oct 13 08:00 commons-text-1.15.0.jar
-rw-r--r-- 1 root root 554 Oct 13 08:00 jackson-annotations-2.21.3.jar
-rw-r--r-- 1 root root 594142 Oct 13 08:00 jackson-core-2.21.3.jar
-rw-r--r-- 1 root root 1702259 Oct 13 08:02 jackson-databind-2.21.3.jar
-rw-r--r-- 1 root root 340068 Oct 13 08:00 snakeyaml-2.6.jar
(jackson-annotations-2.21.3.jar 是当时下载失败的 404 HTML 残片,仅 554 字节;正确文件名是 jackson-annotations-2.21.jar,见第六章。)
二、工具链总览
Commons Configuration 是一个 Java 库,官方没有附带任何命令行可执行程序(发行包 bin 包内只有 jar、apidocs 与许可证文件,tar tzf 可验证)。所谓「CLI 实操」是用 JDK 自带工具直接驱动该库:
| 工具 | 用途 | 本篇场景 |
|---|---|---|
jshell | JDK 自带 REPL,脚本模式 .jsh | 免编译直接读改存 properties |
javac / java -cp | 编译运行单文件验证程序 | XML/INI/YAML/JSON 各源验证 |
jar tf | 列出 jar 内类 | 排查「类在哪个 jar」 |
javap | 反汇编类的方法签名 | 确认 API 是否存在 |
tar tzf / tar xzf | 检查与解压发行包 | 确认 bin 包内容 |
jar tf 快速验证(实测):
jar tf /opt/commons-config/lib/commons-configuration2-2.15.1.jar | grep -c "class"
回显(实测):
401
三、各数据源的 classpath 需求
先给结论表,再逐一实测验证(❌ 报错原文与 ✅ 修复后的输出都在后文):
| 数据源/能力 | 主 jar 外必需 jar | Maven 坐标 |
|---|---|---|
| properties(FileHandler 直连) | 无 | 必选 4 件套即可 |
| XML | 无 | 必选 4 件套即可 |
| INI | 无 | 必选 4 件套即可 |
properties/XML/任意(Parameters Builder 工厂) | + commons-beanutils-1.11.0.jar | commons-beanutils:commons-beanutils:1.11.0(pom 中 optional) |
| YAML(YAMLConfiguration) | + snakeyaml-2.6.jar | org.yaml:snakeyaml:2.6(optional) |
| JSON(JSONConfiguration) | + jackson-databind-2.21.3.jar + jackson-core-2.21.3.jar + jackson-annotations-2.21.jar | com.fasterxml.jackson.core:jackson-databind:2.21.3(optional) |
| XML XPath 表达式引擎 | + commons-jxpath-1.4.0.jar | commons-jxpath:commons-jxpath:1.4.0(optional) |
必选 4 件套(pom 非 optional 依赖,Maven dependency:tree 实测):commons-lang3-3.20.0.jar、commons-text-1.15.0.jar、commons-logging-1.3.6.jar、commons-io-2.22.0.jar。
💡 技术拓展:数据源与依赖/能力对照(语法、层级、类型、热更新维度的完整对比见安装指南)
数据源 Configuration 类 额外依赖 层级 原生类型 properties PropertiesConfiguration无 点分扁平 字符串 XML XMLConfiguration无 树形+属性 字符串 INI INIConfiguration无 section 分组 字符串 YAML YAMLConfigurationsnakeyaml 树形 数字/布尔保留 JSON JSONConfigurationjackson-databind 树形 数字/布尔保留 本文逐一实测 properties/XML/INI/YAML/JSON 五条路线。
四、jshell 实操:免编译读写 properties
4.1 启动 jshell
jshell --version
回显(实测):
jshell 17.0.20.1
--class-path 支持通配符,把整个 lib 目录挂进 REPL:
jshell --class-path "/opt/commons-config/lib/*" jshell-session.jsh
4.2 脚本内容(jshell-session.jsh)
import org.apache.commons.configuration2.*;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.io.FileHandler;
import java.nio.file.*;
Files.write(Path.of("cli.properties"), "app.name = cli-demo\napp.port = 9090\nservers = 10.0.0.1,10.0.0.2\n".getBytes());
var props = new PropertiesConfiguration();
props.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
var handler = new FileHandler(props);
handler.setFileName("cli.properties");
handler.load();
System.out.println("app.name = " + props.getString("app.name"));
System.out.println("app.port = " + props.getInt("app.port"));
System.out.println("servers = " + props.getList("servers"));
props.setProperty("app.port", 9191);
handler.save();
System.out.println("saved, cli.properties content:");
Files.readAllLines(Path.of("cli.properties")).forEach(System.out::println);
/exit
回显(实测):
app.name = cli-demo
app.port = 9090
servers = [10.0.0.1,10.0.0.2]
saved, cli.properties content:
app.name = cli-demo
app.port = 9191
servers = 10.0.0.1,10.0.0.2
properties 读写闭环完成:9090 被改为 9191 并落盘。
4.3 jshell 的四个坑(全部实测踩过)
坑 1:.jsh 脚本里语句必须带分号。jshell 交互模式可以省略分号,但脚本模式的花括号块内省略分号直接报错(实测):
Error:
';' expected
out.println("app.name = cli-demo")
^
坑 2:import a.b.* 不覆盖子包。import org.apache.commons.configuration2.* 之后用 DefaultListDelimiterHandler 报错(实测):
Error:
cannot find symbol
symbol: class DefaultListDelimiterHandler
props.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
它实际在 convert 子包,需显式 import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;。
坑 3:stdin 非交互(管道喂命令)时 REPL 抑制回显,jshell --class-path "/opt/commons-config/lib/*" - < cmds.txt 这类管道方式成功语句不打印结果;文档化的做法是在脚本里 System.out.println 显式输出。
坑 4:--execution local 会让 --class-path 失效,报大量 NoClassDefFoundError;jshell 保持默认执行引擎即可。
五、javac + java -cp 实操
5.1 单文件编译运行模板
cd /opt/commons-config/demos
javac -encoding UTF-8 -cp "/opt/commons-config/lib/*" -d classes src/com/example/XmlDemo.java
java -Dfile.encoding=UTF-8 -cp "classes:/opt/commons-config/lib/*" com.example.XmlDemo
说明:-encoding UTF-8 必带(否则中文注释源码报 unmappable character);-cp 通配符 lib/* 只匹配 jar 文件本身,不递归子目录;classes: 与 lib/* 用冒号分隔(Linux 分隔符)。
5.2 XML 读写(无需额外 jar)
XmlDemo.java 生成演示文件、读取元素与属性、改值回写:
package com.example;
import org.apache.commons.configuration2.XMLConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import java.io.PrintWriter;
public class XmlDemo {
public static void main(String[] args) throws Exception {
// 准备演示 XML
try (PrintWriter out = new PrintWriter("database.xml", "UTF-8")) {
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.println("<!-- 数据库连接配置,由 XmlDemo 生成 -->");
out.println("<database>");
out.println(" <url>jdbc:mysql://localhost:3306/demo</url>");
out.println(" <user timeout=\"30\">root</user>");
out.println(" <pool>");
out.println(" <maxTotal>20</maxTotal>");
out.println(" <minIdle>2</minIdle>");
out.println(" </pool>");
out.println(" <servers>");
out.println(" <server>db1.example.com</server>");
out.println(" <server>db2.example.com</server>");
out.println(" </servers>");
out.println("</database>");
}
FileBasedConfigurationBuilder<XMLConfiguration> builder =
new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
.configure(new Parameters().xml()
.setFileName("database.xml")
.setEncoding("UTF-8"));
XMLConfiguration xml = builder.getConfiguration();
// 2.x 的键从根元素下一级开始写(不同于 1.x 的 database.url 写法)
System.out.println("url = " + xml.getString("url"));
System.out.println("user = " + xml.getString("user"));
System.out.println("user[@timeout] = " + xml.getInt("user[@timeout]"));
System.out.println("pool.maxTotal = " + xml.getInt("pool.maxTotal"));
System.out.println("servers = " + xml.getList("servers.server"));
// 修改并回写
xml.setProperty("pool.maxTotal", 50);
builder.save();
System.out.println("---- database.xml after save ----");
new java.io.BufferedReader(new java.io.FileReader("database.xml"))
.lines().forEach(System.out::println);
}
}
回显(实测):
url = jdbc:mysql://localhost:3306/demo
user = root
user[@timeout] = 30
pool.maxTotal = 20
servers = [db1.example.com, db2.example.com]
---- database.xml after save ----
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 数据库连接配置,由 XmlDemo 生成 --><database>
<url>jdbc:mysql://localhost:3306/demo</url>
<user timeout="30">root</user>
<pool>
<maxTotal>50</maxTotal>
<minIdle>2</minIdle>
</pool>
<servers>
<server>db1.example.com</server>
<server>db2.example.com</server>
</servers>
</database>
两个关键差异点:键不含根元素名(url 而非 database.url,2.x 键相对根元素下一级);属性用 user[@timeout] 语法读取。
5.3 INI 分节(无需额外 jar)
IniDemo.java:
package com.example;
import org.apache.commons.configuration2.INIConfiguration;
import org.apache.commons.configuration2.SubnodeConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
public class IniDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("app.ini", "UTF-8")) {
out.println("[mysql]");
out.println("host = 127.0.0.1");
out.println("port = 3306");
out.println("[redis]");
out.println("host = 127.0.0.1");
out.println("port = 6379");
out.println("database = 0");
}
INIConfiguration ini = new INIConfiguration();
new FileHandler(ini).load("app.ini");
// 全局视图:section.key 前缀访问
System.out.println("mysql.host = " + ini.getString("mysql.host"));
System.out.println("mysql.port = " + ini.getInt("mysql.port"));
System.out.println("redis.port = " + ini.getInt("redis.port"));
// 也可以取单个 section 的子配置
SubnodeConfiguration redis = ini.getSection("redis");
System.out.println("redis(host) = " + redis.getString("host"));
System.out.println("sections = " + ini.getSections());
}
}
回显(实测):
mysql.host = 127.0.0.1
mysql.port = 3306
redis.port = 6379
redis(host) = 127.0.0.1
sections = [mysql, redis]
六、YAML 与 JSON:先报错,后补依赖
6.1 YAML:缺 snakeyaml 的报错与修复
用只含必选依赖的 lib-core 目录(5 个 jar,无 snakeyaml)运行 YamlDemo(实测):
java -cp "classes:/opt/commons-config/lib-core/*" com.example.YamlDemo
回显(实测,节选首 3 行):
Exception in thread "main" java.lang.NoClassDefFoundError: org/yaml/snakeyaml/constructor/BaseConstructor
at com.example.YamlDemo.main(YamlDemo.java:25)
Caused by: java.lang.ClassNotFoundException: org.yaml.snakeyaml.constructor.BaseConstructor
补 snakeyaml:官方 pom 声明 org.yaml:snakeyaml:2.6(optional)。
cd /opt/commons-config/lib
curl -sSLO https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.6/snakeyaml-2.6.jar
ls -l snakeyaml-2.6.jar
回显(实测):
-rw-r--r-- 1 root root 340068 Oct 13 08:00 snakeyaml-2.6.jar
YamlDemo.java:
package com.example;
import org.apache.commons.configuration2.YAMLConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
import java.util.List;
public class YamlDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("app.yaml", "UTF-8")) {
out.println("database:");
out.println(" url: jdbc:mysql://localhost:3306/demo");
out.println(" pool:");
out.println(" maxTotal: 20");
out.println(" minIdle: 2");
out.println("servers:");
out.println(" - db1.example.com");
out.println(" - db2.example.com");
out.println("feature:");
out.println(" enabled: true");
out.println("timeout: 30s");
}
YAMLConfiguration yaml = new YAMLConfiguration();
new FileHandler(yaml).load("app.yaml");
System.out.println("database.url = " + yaml.getString("database.url"));
System.out.println("database.pool.maxTotal = " + yaml.getInt("database.pool.maxTotal"));
List<Object> servers = yaml.getList("servers");
System.out.println("servers = " + servers);
System.out.println("feature.enabled = " + yaml.getBoolean("feature.enabled"));
System.out.println("timeout = " + yaml.getString("timeout"));
new FileHandler(yaml).save("app-roundtrip.properties");
System.out.println("---- round-trip to properties ----");
new java.io.BufferedReader(new java.io.FileReader("app-roundtrip.properties"))
.lines().forEach(System.out::println);
}
}
回显(实测):
database.url = jdbc:mysql://localhost:3306/demo
database.pool.maxTotal = 20
servers = [db1.example.com, db2.example.com]
feature.enabled = true
timeout = 30s
---- round-trip to properties ----
database:
pool:
maxTotal: 20
minIdle: 2
url: jdbc:mysql://localhost:3306/demo
servers:
- db1.example.com
- db2.example.com
feature:
enabled: true
timeout: 30s
注意 round-trip 输出仍是 YAML 语法:保存格式由 Configuration 类决定(YAMLConfiguration 永远写 YAML),与目标文件扩展名无关——这是命令行验证时常见的误判点。另外 YAML 数字/布尔保留原生类型:maxTotal 读出即 int,enabled 读出即 boolean。
6.2 JSON:缺 jackson 的报错与修复
同样先无依赖运行(实测,节选首 2 行):
Exception in thread "main" java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/JavaType
at com.example.JsonDemo.main(JsonDemo.java:23)
补 jackson:官方 pom 声明 jackson-databind 2.21.3。实测发现 Maven 中央仓库没有 jackson-annotations 2.21.3(curl 验证 404,Jackson 自 2.20 起 annotations 不再带 patch 号,bom 里 jackson.version.annotations=2.21),手动组 classpath 必须用 jackson-annotations-2.21.jar:
cd /opt/commons-config/lib
B=https://repo1.maven.org/maven2
curl -sSLO "$B/com/fasterxml/jackson/core/jackson-databind/2.21.3/jackson-databind-2.21.3.jar"
curl -sSLO "$B/com/fasterxml/jackson/core/jackson-core/2.21.3/jackson-core-2.21.3.jar"
curl -sSLO "$B/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar"
curl -sI "$B/com/fasterxml/jackson/core/jackson-annotations/2.21.3/jackson-annotations-2.21.3.jar" | head -1
回显(实测):
HTTP/2 404
JsonDemo.java:
package com.example;
import org.apache.commons.configuration2.JSONConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
import java.util.List;
public class JsonDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("app.json", "UTF-8")) {
out.println("{");
out.println(" \"database\": {");
out.println(" \"url\": \"jdbc:mysql://localhost:3306/demo\",");
out.println(" \"pool\": {\"maxTotal\": 20, \"minIdle\": 2}");
out.println(" },");
out.println(" \"servers\": [\"db1.example.com\", \"db2.example.com\"],");
out.println(" \"feature\": {\"enabled\": true},");
out.println(" \"timeout\": \"30s\"");
out.println("}");
}
JSONConfiguration json = new JSONConfiguration();
new FileHandler(json).load("app.json");
System.out.println("database.url = " + json.getString("database.url"));
System.out.println("database.pool.maxTotal = " + json.getInt("database.pool.maxTotal"));
List<Object> servers = json.getList("servers");
System.out.println("servers = " + servers);
System.out.println("feature.enabled = " + json.getBoolean("feature.enabled"));
}
}
回显(实测):
database.url = jdbc:mysql://localhost:3306/demo
database.pool.maxTotal = 20
servers = [db1.example.com, db2.example.com]
feature.enabled = true
七、jar 与 javap 排查技巧
7.1 确认「类在哪个 jar」
jar tf /opt/commons-config/lib/snakeyaml-2.6.jar | grep "org/yaml/snakeyaml/Yaml.class"
回显(实测):
org/yaml/snakeyaml/Yaml.class
对疑似 jar 逐个执行 jar tf /opt/commons-config/lib/commons-configuration2-2.15.1.jar | grep XPathExpressionEngine 这类命令即可定位;grep -c 还能统计整个 jar 的类数量(见第二章)。
7.2 确认「方法是否存在」
不确定 API 时用 javap 看签名,避免凭记忆臆造(实测):
javap -cp /opt/commons-config/lib/commons-configuration2-2.15.1.jar org.apache.commons.configuration2.io.FileHandler | grep -E "FileHandler\(|load|save"
回显(实测节选):
public org.apache.commons.configuration2.io.FileHandler();
public org.apache.commons.configuration2.io.FileHandler(org.apache.commons.configuration2.io.FileBased);
public void load() throws org.apache.commons.configuration2.ex.ConfigurationException;
public void load(java.lang.String) throws org.apache.commons.configuration2.ex.ConfigurationException;
public void save() throws org.apache.commons.configuration2.ex.ConfigurationException;
public void save(java.lang.String) throws org.apache.commons.configuration2.ex.ConfigurationException;
由此可实锤:load()/save() 返回 void,不能链式调用;new FileHandler(配置对象) 构造器存在。
7.3 一个真实的坑:load(String) 后 save() 失败
先 handler.load("cli.properties") 再直接 handler.save() 会报错(jshell 实测):
Exception org.apache.commons.configuration2.ex.ConfigurationException: No file location has been set.
at FileHandler.save (FileHandler.java:887)
at FileHandler.save (FileHandler.java:845)
原因:load(String)/save(String) 是一次性重载,不会把路径写入 handler 的定位器;save() 无参版本要求先 setFileName(...)。修复写法:
handler.setFileName("cli.properties");
handler.load();
handler.save();
八、常见问题(FAQ)
Q1:jshell 脚本报 ';' expected?
脚本模式的块内语句必须分号结尾,见 4.3 坑 1。交互模式下单行表达式可省略分号,但贴成 .jsh 文件执行就会翻车。
Q2:NoClassDefFoundError 速查?
按缺失类名对号入座(全部实测复现过):
| 缺失类前缀 | 缺的 jar | 补法 |
|---|---|---|
org/apache/commons/beanutils/ | commons-beanutils-1.11.0.jar | 用了 Parameters/Builder 工厂时必补 |
org/yaml/snakeyaml/ | snakeyaml-2.6.jar | YAML 数据源 |
com/fasterxml/jackson/databind/ | jackson-databind-2.21.3.jar + jackson-core-2.21.3.jar + jackson-annotations-2.21.jar | JSON 数据源 |
org/apache/commons/jxpath/ | commons-jxpath-1.4.0.jar | XPathExpressionEngine |
Q3:java -cp 写了目录却报 ClassNotFoundException?
通配符必须带 * 且不能加递归:-cp "classes:/opt/commons-config/lib/*" 正确;-cp "/opt/commons-config/lib" 只当普通目录(找 .class 不找 jar);lib/**/*.jar 不被支持。另外 Windows 下分隔符是分号,Linux 是冒号。
Q4:javac 报 unmappable character (0xE9) for encoding US-ASCII?
平台编码不是 UTF-8(echo $LANG 为空或 C)。解法:javac 加 -encoding UTF-8,运行带 -Dfile.encoding=UTF-8,或 export LANG=C.UTF-8 一劳永逸。
Q5:jshell 里 var 声明的配置对象为什么能直接用?
jshell 自动推断类型(var props → PropertiesConfiguration),与编译期写法等价;脚本调试通过后再固化成 .java 文件,本篇 5.1 的模板即是固化路径。
九、参考链接
- JShell 用户指南(脚本模式与反馈模式):https://docs.oracle.com/en/java/javase/17/jshell/introduction-jshell.html
- java 命令 classpath 通配符官方说明:https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html
- Apache Commons Configuration 2.15.1 Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/index.html
- 2.x 用户指南·各 Configuration 实现(数据源矩阵):https://commons.apache.org/proper/commons-configuration/userguide/howto_configurationbuilders.html
- Maven 中央仓库 commons-configuration2 目录:https://repo1.maven.org/maven2/org/apache/commons/commons-configuration2/2.15.1/