Apache Commons Configuration 快速入门与集成示例
以 2.15.1 实测 Apache Commons Configuration 快速入门:Maven 依赖接入、Configurations 一行式门面与自动保存、环境变量与系统属性源、组合配置优先级、不重启自动重载,示例输出双系统实测。
Apache Commons Configuration 快速入门与集成示例
目标:以最快路径跑通 Apache Commons Configuration 2.15.1——第一个读写程序、Maven 依赖接入、
Configurations一行式门面与自动保存、XML 读写、环境变量/系统属性源、组合配置优先级,最后实测「改配置文件不重启进程、读到新值」,每段代码与输出均已在 Ubuntu 22.04 / CentOS 8(JDK 17 + Maven)实测通过。
一、环境信息
本篇所有示例在以下两套环境编译运行并交叉验证,命令与代码完全一致:
| 项目 | Ubuntu 22.04 环境 | CentOS 8 环境 |
|---|---|---|
| JDK | OpenJDK 17.0.20.1 | OpenJDK 17.0.1 |
| Maven | 3.6.3 | 3.5.4 |
| 组件版本 | org.apache.commons:commons-configuration2:2.15.1 | 同左 |
- 运行时依赖 jar(放在
/opt/commons-config/lib/):commons-configuration2-2.15.1.jar、commons-lang3-3.20.0.jar、commons-text-1.15.0.jar、commons-logging-1.3.6.jar、commons-io-2.22.0.jar、commons-beanutils-1.11.0.jar。前四个是官方 pom 声明的传递依赖(第三章依赖树实测一致);commons-beanutils 在官方 pom 中标记<optional>,但Parameters/Builder API 运行期必需,手动路线要单独放进 classpath - 实验目录:
/opt/commons-config/demos(源码src/com/example/,编译输出classes/,运行目录run/——示例程序会把生成的配置文件写在运行时的当前目录) - 编译命令模板(在
/opt/commons-config/demos下执行):
javac -encoding UTF-8 -cp "/opt/commons-config/lib/*" -d classes src/com/example/QuickStart.java
- 运行命令模板(在
/opt/commons-config/demos/run下执行):
java -Dfile.encoding=UTF-8 -cp "../classes:/opt/commons-config/lib/*" com.example.QuickStart
二、五分钟跑通第一个程序
2.x 读取 properties 文件的标准组合是 PropertiesConfiguration(内存中的配置对象)加 FileHandler(负责文件定位、加载与回写),读、改、存三步都走它。
💡 技术拓展:如果项目尚未确定用哪个配置库,可参考下表定位 Commons Configuration2 的位置。本文选用 Commons Configuration2。
库 归属 多格式支持 变量插值 动态重载 适用场景 JDK java.util.PropertiesJDK 自带 仅 properties 无 无 单文件、零依赖 Spring EnvironmentSpring 生态 随框架 支持占位符 配合 Spring 实现 Spring/Spring Boot 项目 Typesafe Config(Lightbend config) Akka/Play 系 properties/JSON/HOCON 是 无内置 JVM 通用、HOCON 语法偏好 Commons Configuration2 Apache Commons properties/XML/JSON/YAML/INI/JNDI/DB 等 ${prefix:key}内建 ReloadingController 独立运行的 Java 程序、多源组合
表中 ${prefix:key} 是 Commons Configuration2 的变量插值语法,运行期自动解析:${env:HOME} 解析为当前用户主目录 /root、${sys:java.home} 解析为 JDK 安装目录(Ubuntu 22.04 实测为 /usr/lib/jvm/java-17-openjdk-amd64),第六章有完整实测。
2.1 完整示例代码
package com.example;
import java.io.FileWriter;
import java.util.Properties;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.FileHandler;
public class QuickStart {
public static void main(String[] args) throws ConfigurationException, java.io.IOException {
// 先手工生成一份 app.properties 作为演示数据
Properties p = new Properties();
p.setProperty("db.url", "jdbc:mysql://localhost:3306/demo");
p.setProperty("db.user", "root");
p.setProperty("db.pool.maxTotal", "20");
try (FileWriter w = new FileWriter("app.properties")) {
p.store(w, "generated by QuickStart");
}
// 2.x 的读取方式:PropertiesConfiguration + FileHandler
PropertiesConfiguration props = new PropertiesConfiguration();
FileHandler handler = new FileHandler(props);
handler.setFileName("app.properties");
handler.load();
Configuration cfg = props;
System.out.println("db.url = " + cfg.getString("db.url"));
System.out.println("db.user = " + cfg.getString("db.user"));
System.out.println("db.pool.maxTotal = " + cfg.getInt("db.pool.maxTotal"));
System.out.println("not.exist = " + cfg.getString("not.exist", "default"));
// 修改并保存,再从磁盘重新加载验证
cfg.setProperty("db.user", "admin");
handler.save();
handler.load();
System.out.println("after save, db.user = " + cfg.getString("db.user"));
}
}
2.2 编译与运行
cd /opt/commons-config/demos
javac -encoding UTF-8 -cp "/opt/commons-config/lib/*" -d classes src/com/example/QuickStart.java
cd run
java -Xmx256m -Dfile.encoding=UTF-8 -cp "../classes:/opt/commons-config/lib/*" com.example.QuickStart
编译无输出即成功。运行加 -Xmx256m 顺带验证 256MB 小堆进程完全够用,实测输出(Ubuntu 22.04 与 CentOS 8 逐行一致):
db.url = jdbc:mysql://localhost:3306/demo
db.user = root
db.pool.maxTotal = 20
not.exist = default
after save, db.user = admin
2.3 输出解读
getInt("db.pool.maxTotal")直接拿到int20,字符串"20"自动转型,这是它比java.util.Properties顺手的地方getString("not.exist", "default"):key 不存在时返回第二个参数的默认值;不带默认值的getString("not.exist")则返回nullhandler.save()后紧跟handler.load()从磁盘重读,db.user仍是admin,证明修改真实落盘
如果照 1.x 教程写 new PropertiesConfiguration("app.properties") 会直接编译报错,2.x 必须走 FileHandler,报错原文与迁移写法见第九章 Q1。
三、接入 Maven 工程
生产项目更常见的方式是 Maven 坐标引入,依赖与打包过程如下实测。
3.1 声明依赖
实验工程位于 /opt/commons-config/demo,pom.xml 全文:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>config-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.15.1</version>
</dependency>
</dependencies>
</project>
一个容易踩的坑:只用 Configuration、FileHandler 这类基础 API 时上面这样就够了;一旦用到 Parameters/Builder API(第五章的 XML 读取、第八章的自动重载都会用),Maven 也不会自动带入 commons-beanutils(官方 pom 标记 <optional>),需要在 pom.xml 里显式补一条:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.11.0</version>
</dependency>
缺失时运行期报 NoClassDefFoundError: org/apache/commons/beanutils/BeanIntrospector,本专栏安装指南 FAQ 有该报错实测原文。
3.2 依赖树与打包实测
cd /opt/commons-config/demo
mvn dependency:tree
输出节选(实测首尾,中间的插件下载行略):
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ config-demo ---
[INFO] com.example:config-demo:jar:1.0.0
[INFO] \- org.apache.commons:commons-configuration2:jar:2.15.1:compile
[INFO] +- org.apache.commons:commons-lang3:jar:3.20.0:compile
[INFO] +- org.apache.commons:commons-text:jar:1.15.0:compile
[INFO] +- commons-logging:commons-logging:jar:1.3.6:compile
[INFO] \- commons-io:commons-io:jar:2.22.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
四个传递依赖与第一章手动路线的 jar 清单完全对应,commons-beanutils 果然不在树里。
打包:
mvn clean package
输出节选(实测首尾):
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ config-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /opt/commons-config/demo/target/classes
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ config-demo ---
[INFO] Building jar: /opt/commons-config/demo/target/config-demo-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.585 s
[INFO] Finished at: 2026-10-13T09:01:18Z
[INFO] ------------------------------------------------------------------------
target/config-demo-1.0.0.jar 即业务 jar;运行时把它和四个传递依赖 jar 一起放进 classpath 即可,等价于第二、四章的手动路线。
四、Configurations 门面:一行式读取与自动保存
org.apache.commons.configuration2.builder.fluent.Configurations 是 2.x 的快捷门面:读取一行代码搞定,保存走 propertiesBuilder 等 Builder 方法,还能开启自动落盘。
4.1 门面方法速览
configs.properties("app.properties")/configs.xml("a.xml")/configs.ini("a.ini"):一行式读取,返回只读用途的配置对象configs.propertiesBuilder("app.properties"):返回FileBasedConfigurationBuilder,支持save()与setAutoSave(true)- 记住分工:便捷方法只管读,保存一律走 Builder
4.2 完整示例代码
package com.example;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Configurations;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ConfigurationsDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("app.properties", "UTF-8")) {
out.println("app.name = order-service");
out.println("app.port = 8080");
out.println("app.debug = false");
out.println("app.ratio = 0.75");
}
// 一次性读取入口:Configurations(2.x 推荐的快捷门面)
Configurations configs = new Configurations();
Configuration cfg = configs.properties("app.properties");
System.out.println("app.name = " + cfg.getString("app.name"));
System.out.println("app.port = " + cfg.getInt("app.port"));
System.out.println("app.debug = " + cfg.getBoolean("app.debug"));
System.out.println("app.ratio = " + cfg.getDouble("app.ratio"));
// Configurations 只负责读;保存要用 propertiesBuilder + builder.save()
FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
configs.propertiesBuilder("app.properties");
PropertiesConfiguration editable = builder.getConfiguration();
editable.setProperty("app.port", 8081);
builder.save();
System.out.println("after builder.save, app.port = "
+ configs.properties("app.properties").getInt("app.port"));
// 开启 autoSave:每次修改自动落盘,无需手动 save()
builder.setAutoSave(true);
editable.setProperty("app.name", "order-service-v2");
System.out.println("after autoSave, app.name on disk = "
+ Files.readAllLines(Paths.get("app.properties")).stream()
.filter(l -> l.startsWith("app.name")).findFirst().orElse("?"));
// XML 同样有一行式入口(2.x 的键相对根元素下一级,根元素 cache 不出现在键里)
try (PrintWriter out = new PrintWriter("mini.xml", "UTF-8")) {
out.println("<cache><ttl>300</ttl></cache>");
}
System.out.println("xml ttl = " + configs.xml("mini.xml").getInt("ttl"));
}
}
4.3 实测输出与落盘验证
app.name = order-service
app.port = 8080
app.debug = false
app.ratio = 0.75
after builder.save, app.port = 8081
after autoSave, app.name on disk = app.name = order-service-v2
xml ttl = 300
程序结束后 cat app.properties 实测,两处修改都已真实写到磁盘:
app.name = order-service-v2
app.port = 8081
app.debug = false
app.ratio = 0.75
三个要点:
builder.save()把app.port改成 8081 后,用全新的configs.properties("app.properties")重读,读到的是新值 8081setAutoSave(true)之后连save()都不用调了,setProperty("app.name", "order-service-v2")一执行磁盘立刻更新- 对
configs.properties("app.properties")返回的对象直接保存会报No file location has been set.(实测原文见第九章 Q2),这是新手最高频的坑
五、读写 XML 配置
XML 比 properties 多了层级和属性,2.x 用 FileBasedConfigurationBuilder 加 Parameters().xml() 读取,可指定编码、可回写。
5.1 示例代码
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 org.apache.commons.configuration2.ex.ConfigurationException;
import java.io.BufferedReader;
import java.io.FileReader;
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 BufferedReader(new FileReader("database.xml"))
.lines().forEach(System.out::println);
}
}
5.2 实测输出与键规则
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>
键规则四条(Ubuntu 22.04 与 CentOS 8 输出一致):
- 键从根元素下一级开始:根元素
database不出现在键里,写url而不是database.url;写错层级会报NoSuchElementException(实测原文见第九章 Q3) - XML 属性用
[@属性名]追加:user[@timeout]取到 30 - 重复标签天然成列表:
servers.server一次取回两个值 setEncoding("UTF-8")必须显式设置,中文注释回写才不乱码;保存后注释行与<database>会贴到同一行(上面输出可见),属于库的序列化风格,不影响解析
六、环境变量与系统属性源
除了文件,Commons Configuration 内建了读取 JVM 系统属性与 OS 环境变量的 Configuration 实现,常用于部署期覆盖文件里的默认值。
💡 技术拓展:Commons Configuration2 常用配置来源对比
来源 实现类 key 语义 读写 典型用途 .properties 文件 PropertiesConfiguration 文件内的 key 读+写 应用主配置 .xml 文件 XMLConfiguration 相对根元素下一级 读+写 结构化配置 JVM 系统属性 SystemConfiguration java -D定义的属性名读系统数据 部署期覆盖 OS 环境变量 EnvironmentConfiguration 环境变量名,区分大小写 读系统数据 部署平台注入变量 内存 Map MapConfiguration Map 的 key 读+写 测试与运行时叠加层
6.1 示例代码(含变量插值)
package com.example;
import org.apache.commons.configuration2.EnvironmentConfiguration;
import org.apache.commons.configuration2.MapConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.SystemConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class EnvSysDemo {
public static void main(String[] args) throws Exception {
// 系统属性源:key 即 JVM 系统属性名
SystemConfiguration sys = new SystemConfiguration();
System.out.println("sys:java.version = " + sys.getString("java.version"));
System.out.println("sys:user.dir = " + sys.getString("user.dir"));
// 环境变量源:key 即环境变量名,大小写敏感(Linux)
EnvironmentConfiguration env = new EnvironmentConfiguration();
System.out.println("env:PATH = " + env.getString("PATH"));
System.out.println("env:HOME = " + env.getString("HOME"));
System.out.println("env:NOT_EXIST = " + env.getString("NOT_EXIST", "unset"));
// copy 的方向:mc.copy(sys) 表示把 sys 的内容拷贝进 mc
Map<String, Object> file = new HashMap<>();
file.put("java.version", "from-file-override-me");
MapConfiguration mc = new MapConfiguration(file);
mc.copy(sys);
System.out.println("after mc.copy(sys), java.version = " + mc.getString("java.version"));
// 插值:配置文件里写 ${env:HOME} / ${sys:java.home},读取时自动替换为真实值
try (PrintWriter w = new PrintWriter("interp.properties", "UTF-8")) {
w.println("work.dir=${env:HOME}");
w.println("runtime.home=${sys:java.home}");
}
PropertiesConfiguration interp = new PropertiesConfiguration();
new FileHandler(interp).load("interp.properties");
System.out.println("work.dir = " + interp.getString("work.dir"));
System.out.println("runtime.home = " + interp.getString("runtime.home"));
}
}
末尾 4 行演示变量插值:interp.properties 里写的 ${env:HOME}、${sys:java.home} 是 Commons Configuration 的官方插值表达式(不是等待替换的占位符模板),env: 前缀查环境变量、sys: 前缀查 JVM 系统属性,getString 读取时自动解析成真实值。
6.2 双系统实测对照
Ubuntu 22.04(OpenJDK 17.0.20.1):
sys:java.version = 17.0.20.1
sys:user.dir = /opt/commons-config/demos/run
env:PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
env:HOME = /root
env:NOT_EXIST = unset
after mc.copy(sys), java.version = 17.0.20.1
work.dir = /root
runtime.home = /usr/lib/jvm/java-17-openjdk-amd64
CentOS 8(OpenJDK 17.0.1):
sys:java.version = 17.0.1
sys:user.dir = /opt/commons-config/demos/run
env:PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
env:HOME = /root
env:NOT_EXIST = unset
after mc.copy(sys), java.version = 17.0.1
work.dir = /root
runtime.home = /usr/lib/jvm/java-17-openjdk-17.0.1.0.12-2.el8_5.x86_64
两套系统只有 java.version 与 java.home 随各自 JDK 安装不同,其余逐行一致——这正说明同一份代码在不同环境读到的是各自环境的真值。
6.3 使用要点
- Linux 环境变量区分大小写:
NOT_EXIST不存在,getString("NOT_EXIST", "unset")返回默认值unset;不带默认值则返回null(打印 null,不抛异常)。排查某变量是否存在可先执行printenv HOME,实测返回/root;查不存在的变量printenv NOT_EXIST无输出、退出码 1 copy的方向容易记反:mc.copy(sys)是把sys的内容拷进mc,拷完后mc里原来的"from-file-override-me"被覆盖为真实版本号- 插值只解析读到的值:
work.dir在磁盘文件里就是字面量${env:HOME},读进内存后才变成/root
七、组合配置与优先级叠加
CombinedConfiguration 把多个 Configuration 叠成一个视图,同名 key 谁生效由 Combiner 决定,是「默认值 + 环境覆盖」模式的标准解法。
7.1 示例代码
package com.example;
import org.apache.commons.configuration2.CombinedConfiguration;
import org.apache.commons.configuration2.MapConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.configuration2.tree.OverrideCombiner;
import org.apache.commons.configuration2.tree.UnionCombiner;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class ComboDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("base.properties", "UTF-8")) {
out.println("db.url=jdbc:mysql://localhost:3306/demo");
out.println("db.user=root");
out.println("app.name=demo-app");
out.println("feature.a=true");
out.println("feature.b=false");
}
try (PrintWriter out = new PrintWriter("override.properties", "UTF-8")) {
out.println("db.url=jdbc:mysql://prod-db:3306/prod");
out.println("feature.b=true");
}
// OverrideCombiner:先加入的优先级最高(override 覆盖 base)
PropertiesConfiguration ovr = new PropertiesConfiguration();
new FileHandler(ovr).load("override.properties");
PropertiesConfiguration base = new PropertiesConfiguration();
new FileHandler(base).load("base.properties");
CombinedConfiguration override = new CombinedConfiguration(new OverrideCombiner());
override.addConfiguration(ovr, "override");
override.addConfiguration(base, "base");
System.out.println("== OverrideCombiner(先加者优先) ==");
System.out.println("db.url = " + override.getString("db.url"));
System.out.println("db.user = " + override.getString("db.user"));
System.out.println("feature.a = " + override.getBoolean("feature.a"));
System.out.println("feature.b = " + override.getBoolean("feature.b"));
System.out.println("app.name = " + override.getString("app.name"));
// UnionCombiner:合并所有源的全部 key
CombinedConfiguration union = new CombinedConfiguration(new UnionCombiner());
union.addConfiguration(base);
union.addConfiguration(ovr);
System.out.println("== UnionCombiner(取并集) ==");
System.out.println("db.url = " + union.getString("db.url"));
System.out.println("app.name = " + union.getString("app.name"));
// 运行时把命令行层排在最前,实现「命令行参数 > 文件」的常见优先级
Map<String, Object> cmd = new HashMap<>();
cmd.put("db.url", "jdbc:mysql://localhost:3306/from-cmdline");
CombinedConfiguration layered = new CombinedConfiguration(new OverrideCombiner());
layered.addConfiguration(new MapConfiguration(cmd), "cmdline");
layered.addConfiguration(base, "file");
System.out.println("== 运行时叠加命令行层 ==");
System.out.println("db.url = " + layered.getString("db.url"));
}
}
7.2 实测输出与覆盖规则
== OverrideCombiner(先加者优先) ==
db.url = jdbc:mysql://prod-db:3306/prod
db.user = root
feature.a = true
feature.b = true
app.name = demo-app
== UnionCombiner(取并集) ==
db.url = jdbc:mysql://localhost:3306/demo
app.name = demo-app
== 运行时叠加命令行层 ==
db.url = jdbc:mysql://localhost:3306/from-cmdline
覆盖规则两条(双系统输出一致):
OverrideCombiner:先加入的层优先。override层先加入,它的db.url(prod-db)与feature.b(true)生效;override没有的db.user、feature.a、app.name由base层兜底UnionCombiner:把所有层的 key 并起来,同名 key 取先加入层的值——这里base先加入,所以db.url是 base 的 localhost 值- 实战套路:
MapConfiguration当命令行层放最前,实现「命令行参数 > 环境特定文件 > 默认文件」的多级优先级,如第三段输出
八、不重启进程的自动重载
配置改了必须重启才能生效?ReloadingFileBasedConfigurationBuilder 加 PeriodicReloadingTrigger 可以做到外部改文件、进程内读到新值。
8.1 示例代码
package com.example;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.ReloadingFileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.reloading.PeriodicReloadingTrigger;
public class ReloadDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("reload.properties", "UTF-8")) {
out.println("thread.pool.size=4");
}
// ReloadingFileBasedConfigurationBuilder 自带文件变化检测
ReloadingFileBasedConfigurationBuilder<PropertiesConfiguration> builder =
new ReloadingFileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(new Parameters().properties()
.setFileName("reload.properties"));
// 周期触发器:每 1 秒驱动一次检测(无事件就什么都不做)
PeriodicReloadingTrigger trigger =
new PeriodicReloadingTrigger(builder.getReloadingController(), null, 1, TimeUnit.SECONDS);
trigger.start();
PropertiesConfiguration cfg = builder.getConfiguration();
System.out.println("first read thread.pool.size = " + cfg.getInt("thread.pool.size"));
// 外部修改文件,进程不重启
Thread.sleep(2500);
Files.write(Paths.get("reload.properties"),
"thread.pool.size=16\nmax.queue=1000\n".getBytes("UTF-8"));
// 重载后 getConfiguration() 返回带新数据的对象,轮询等待生效(最多 6 秒)
for (int i = 0; i < 60; i++) {
cfg = builder.getConfiguration();
if (cfg.getInt("thread.pool.size", -1) == 16) {
break;
}
Thread.sleep(100);
}
System.out.println("after edit thread.pool.size = " + cfg.getInt("thread.pool.size", -1));
System.out.println("new key max.queue = " + cfg.getString("max.queue", "not-yet"));
trigger.stop();
}
}
8.2 实测:改文件不重启读新值
代码里 Files.write 那一步等价于另开一个终端执行 echo 'thread.pool.size=16' > reload.properties(示例为了自包含直接在进程内模拟外部修改,两种方式对重载机制完全相同)。实测输出:
first read thread.pool.size = 4
after edit thread.pool.size = 16
new key max.queue = 1000
进程从启动到结束没有重启,thread.pool.size 从 4 变为 16,新增的 max.queue 也读到了 1000。机制三点:
PeriodicReloadingTrigger只是周期性「驱动检测」,必须start();忘了 start,重载永远不会发生- 检测到变化后,要重新调用
builder.getConfiguration()才能拿到带新数据的对象,一直持有旧引用看不到变化——所以示例用轮询重新获取 - 1 秒检测周期 + 最多 6 秒轮询等待,实测总耗时约 3 秒完成切换;结束时
trigger.stop()释放后台线程
九、常见问题(FAQ)
Q1:照 1.x 教程写 new PropertiesConfiguration("app.properties") 和 cfg.save() 编译不过?
1.x 的「构造即加载」「对象直接 save」API 在 2.x 全部移除,实测 javac 报错:
src/com/example/Legacy.java:7: error: constructor PropertiesConfiguration in class PropertiesConfiguration cannot be applied to given types;
PropertiesConfiguration cfg = new PropertiesConfiguration("app.properties");
^
required: no arguments
found: String
reason: actual and formal argument lists differ in length
src/com/example/Legacy.java:9: error: cannot find symbol
cfg.save();
^
symbol: method save()
location: variable cfg of type PropertiesConfiguration
2 errors
2.x 的等价写法:无参构造 + FileHandler,setFileName() 后 load()/save(),第二章 QuickStart 即完整可运行版本。
Q2:configs.properties("app.properties") 拿到的对象为什么不能直接保存?
Configurations 的便捷读取方法只加载内容、不保留文件定位信息,对返回对象再包一层 FileHandler 调 save() 会实测报错:
org.apache.commons.configuration2.ex.ConfigurationException: No file location has been set.
保存请改用 configs.propertiesBuilder("app.properties") 拿 builder,改完后 builder.save(),或 builder.setAutoSave(true),第四章已实测两条路。
Q3:读 XML 键写 cache.ttl 报 NoSuchElementException?
2.x 的 XML 键相对根元素下一级,根元素不出现在键里。对 <cache><ttl>300</ttl></cache> 写 cache.ttl,实测报错(输出节选):
Exception in thread "main" java.util.NoSuchElementException: Key 'cache.ttl' does not map to an existing object.
at org.apache.commons.configuration2.AbstractConfiguration.throwMissingPropertyException(AbstractConfiguration.java:180)
正确键是 ttl,第四章与第五章都按此规则实测通过。
Q4:EnvironmentConfiguration 读不到某个环境变量?
Linux 环境变量区分大小写,先确认名字拼写。实测 getString("NOT_EXIST", "unset") 返回默认值 unset,不带默认值则返回 null 而不是抛异常,容易把 null 混进后续逻辑。排查时在 shell 执行 printenv HOME 实测返回 /root,而 printenv NOT_EXIST 无输出、退出码 1,即可确认变量确实不存在。
Q5:自动重载迟迟不生效?
三个实测要点:① PeriodicReloadingTrigger 创建后必须 start();② 周期到点后必须重新调 builder.getConfiguration() 拿新对象,持有旧引用永远看不到新值;③ 修改文件用进程内 Files.write 或另一终端编辑均可触发,第八章实测从修改到读到新值约 3 秒(1 秒检测周期 + 轮询间隔)。另注意重载期间文件必须始终可解析,写一半的文件会触发加载异常,生产上建议「写临时文件后原子改名」。
十、参考链接
- Apache Commons Configuration 官网:https://commons.apache.org/proper/commons-configuration/
- 2.x 用户指南·Quick start:https://commons.apache.org/proper/commons-configuration/userguide/quick_start.html
- 2.x 用户指南·Configuration builders(FileBasedConfigurationBuilder 与自动保存):https://commons.apache.org/proper/commons-configuration/userguide/howto_configurationbuilders.html
- 2.x Javadoc(Configurations/ReloadingFileBasedConfigurationBuilder/PeriodicReloadingTrigger):https://commons.apache.org/proper/commons-configuration/apidocs/index.html
- Maven 中央仓库 commons-configuration2 2.15.1:https://repo1.maven.org/maven2/org/apache/commons/commons-configuration2/2.15.1/