Apache Commons Configuration 配置参数详解
解析 Apache Commons Configuration 2.15.1 参数体系:Parameters 路径、编码、异常策略、分隔符、include 控制、XML 表达式引擎、变量插值、自动重载与组合配置覆盖顺序,示例与输出全部实测。
Apache Commons Configuration 配置参数详解
目标:讲透 Apache Commons Configuration 2.15.1 的参数体系——
Parameters各 Builder 参数(路径、编码、异常策略、分隔符、include 控制、XML 表达式引擎)、变量插值${key}、自动重载参数与组合配置覆盖顺序,每项均给可运行示例与实测输出,已在 Ubuntu 22.04(OpenJDK 17)实测通过。
一、环境信息
本篇所有示例在以下环境编译运行(安装步骤见本专栏安装指南,泛泛指引即可,两篇独立可执行):
- 操作系统:Ubuntu 22.04,x86_64
- JDK:OpenJDK 17.0.20.1
- 组件版本: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(可选格式另备 snakeyaml-2.6.jar、jackson-databind-2.21.3.jar 等) - 实验目录:
/opt/commons-config/demos(源码src/com/example/,编译输出classes/) - 编译命令模板:
javac -encoding UTF-8 -cp "/opt/commons-config/lib/*" -d classes src/com/example/QuickStart.java - 运行命令模板:
java -cp "classes:/opt/commons-config/lib/*" com.example.QuickStart
二、参数体系总览
2.x 的配置参数统一由 org.apache.commons.configuration2.builder.fluent.Parameters 工厂创建,按配置类型返回不同的参数接口:properties() 返回 PropertiesBuilderParameters、xml() 返回 XMLBuilderParameters、hierarchical()、ini()、json()、yaml() 等,最后由 FileBasedConfigurationBuilder.configure(...) 消费。
前置依赖提醒:Parameters 是动态代理实现,运行期需要 commons-beanutils,而它在官方 pom 中是 <optional> 依赖,Maven 不会自动带入。缺失时任何 Parameters.properties()/Parameters.xml() 调用都会抛出(实测):
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/beanutils/BeanIntrospector
at org.apache.commons.configuration2.builder.fluent.Parameters.createParametersProxy(Parameters.java:174)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.beanutils.BeanIntrospector
解法:Maven 显式声明 commons-beanutils:commons-beanutils:1.11.0,或手动 classpath 加入 commons-beanutils-1.11.0.jar。
2.1 全部参数类型的公共参数
以下参数在所有 BuilderParameters 上都可用(自 BasicBuilderParameters 继承,javap 实测方法签名):
| 参数方法 | 作用 |
|---|---|
setThrowExceptionOnMissing(boolean) | 缺 key 时抛异常而不是返回 null(异常策略) |
setListDelimiterHandler(ListDelimiterHandler) | 列表分隔策略(切分/禁用) |
setConversionHandler(ConversionHandler) | 取值时的类型转换器 |
setPrefixLookups(Map) / setDefaultLookups(Collection) | 变量插值前缀注册 |
setInterpolator(...) / setParentInterpolator(...) | 自定义插值器 |
setSynchronizer(Synchronizer) | 并发读写锁策略 |
setLogger(ConfigurationLogger) | 内部日志 |
setConfigurationDecoder(ConfigurationDecoder) | 值解码器 |
setBeanHelper(BeanHelper) | Bean 装配辅助(配合 beanutils) |
文件型配置(properties/XML/YAML/JSON/INI)再叠加 FileBasedBuilderParameters 的文件定位与重载参数:setFile、setFileName、setPath、setBasePath、setURL、setEncoding、setLocationStrategy、setFileSystem、setReloadingRefreshDelay、setReloadingDetectorFactory(均 javap 实测存在)。
关于「懒加载」:2.x 没有独立的「懒加载开关」参数。配置对象内部的插值器等组件采用懒初始化,但 BasicConfigurationBuilder.createResult() 在返回配置对象前会统一调用 Initializable.initialize() 完成全部初始化(源码 Initializable.java 注释明确此设计是为了线程安全),因此从 builder 拿到的对象是完整初始化的;只读场景可用 setSynchronizer(NoOpSynchronizer.INSTANCE) 省掉锁开销。请勿寻找或臆造「lazy load=true」之类的参数。
💡 技术拓展:配置来源与参数的对应关系速查(详细对比见安装指南同款表格)
配置类型 Parameters 工厂方法 Configuration 类 文件格式专属参数 properties params.properties()PropertiesConfigurationsetIncludesAllowed、setIOFactory、setLayoutXML params.xml()XMLConfigurationsetValidating、setSchemaValidation、setExpressionEngine、setEntityResolverINI params.ini()INIConfiguration继承层级参数 JSON params.json()JSONConfiguration继承层级参数(需 jackson-databind) YAML params.yaml()YAMLConfiguration继承层级参数(需 snakeyaml) 本文主线为 properties 与 XML 两个参数族。
三、路径参数
文件型配置支持四种路径写法,语义不同:setFileName 是相对路径(相对基础路径或当前目录)、setFile 传 File 对象、setPath 传完整路径字符串、setBasePath + setFileName 组合先定基础路径再定文件名。
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.Parameters;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import java.io.File;
import java.io.PrintWriter;
import java.util.NoSuchElementException;
public class ParamDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("params.properties", "UTF-8")) {
out.println("app.name = order-service");
out.println("app.port = 8080");
out.println("ips = a, b, c");
}
try (PrintWriter out = new PrintWriter("secret.properties", "UTF-8")) {
out.println("db.pwd = ${secret:base64:MTIzNDU2}");
}
Parameters params = new Parameters();
// 1. 路径参数的四种写法
Configuration c1 = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setFileName("params.properties"))
.getConfiguration();
System.out.println("setFileName(相对路径) app.name = " + c1.getString("app.name"));
Configuration c2 = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setFile(new File("params.properties").getAbsoluteFile()))
.getConfiguration();
System.out.println("setFile(绝对 File 对象) app.port = " + c2.getInt("app.port"));
Configuration c3 = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setPath(
new File("params.properties").getAbsolutePath()))
.getConfiguration();
System.out.println("setPath(完整路径字符串) app.port = " + c3.getInt("app.port"));
Configuration c4 = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties()
.setBasePath(System.getProperty("user.dir"))
.setFileName("params.properties"))
.getConfiguration();
System.out.println("setBasePath+setFileName app.port = " + c4.getInt("app.port"));
// 2. 异常策略:缺 key 抛 NoSuchElementException 而非返回 null
Configuration strict = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setFileName("params.properties")
.setThrowExceptionOnMissing(true)
.setListDelimiterHandler(new DefaultListDelimiterHandler(',')))
.getConfiguration();
try {
strict.getString("not.exist");
} catch (NoSuchElementException e) {
System.out.println("strict 模式抛出异常 -> " + e.getMessage());
}
System.out.println("strict getList(ips) = " + strict.getList("ips"));
// 3. setIncludesAllowed(false):禁用 include 指令
Configuration noInclude = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setFileName("params.properties")
.setIncludesAllowed(false))
.getConfiguration();
System.out.println("includesAllowed=false app.name = " + noInclude.getString("app.name"));
// 4. setPrefixLookups:注册自定义插值前缀(示例:解密 secret: 前缀的 base64 值)
Configuration withLookup = new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(params.properties().setFileName("secret.properties")
.setPrefixLookups(java.util.Map.of("secret", variable -> {
String b64 = variable.replaceFirst("^base64:", "");
return new String(java.util.Base64.getDecoder().decode(b64));
})))
.getConfiguration();
System.out.println("自定义 Lookup 插值 db.pwd = " + withLookup.getString("db.pwd"));
}
}
编译运行(样例工程已内置于 /opt/commons-config/demos):
cd /opt/commons-config/demos
javac -encoding UTF-8 -cp "/opt/commons-config/lib/*" -d classes src/com/example/ParamDemo.java
java -Dfile.encoding=UTF-8 -cp "classes:/opt/commons-config/lib/*" com.example.ParamDemo
回显(实测):
setFileName(相对路径) app.name = order-service
setFile(绝对 File 对象) app.port = 8080
setPath(完整路径字符串) app.port = 8080
setBasePath+setFileName app.port = 8080
strict 模式抛出异常 -> Key 'not.exist' does not map to an existing object.
strict getList(ips) = [a, b, c]
includesAllowed=false app.name = order-service
自定义 Lookup 插值 db.pwd = 123456
逐项说明:
setFileName("params.properties"):相对路径,默认在当前工作目录(及 classpath 的定位策略链)中查找;setFile(绝对 File 对象)/setPath(完整路径字符串):两者等价,绕过定位策略直接用绝对路径;setBasePath + setFileName:basePath拼接fileName构成最终位置,适合「配置目录可切换」的场景。
运行过程中 stderr 还会出现一条 beanutils 的 INFO 日志(实测):
Oct 13, 2026 8:09:51 AM org.apache.commons.beanutils.FluentPropertyBeanIntrospector introspect
INFO: Error when creating PropertyDescriptor for public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)! Ignoring this property.
这是 beanutils 内省器跳过 setProperty 重载的正常行为,对功能无影响,可忽略或调低日志级别。
四、编码参数
setEncoding 决定读取与保存使用的字符集。2.x 中 PropertiesConfiguration.DEFAULT_ENCODING = ISO-8859-1(源码常量,遵循 JDK Properties 规范),中文内容必须显式设置编码。
用 GBK 写入的文件分别按默认编码与 GBK 读取(EncodeDemo.java):
package com.example;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class EncodeDemo {
public static void main(String[] args) throws Exception {
// 用 GBK 写一个含中文的 properties 文件(模拟历史遗留系统文件)
try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
new FileOutputStream("gbk.properties"), "GBK"))) {
out.println("# GBK 编码文件");
out.println("app.title = 订单服务");
out.println("app.owner = 运维平台组");
}
// 不指定编码:PropertiesConfiguration 默认 ISO-8859-1,中文按字节错读
PropertiesConfiguration raw = new PropertiesConfiguration();
new FileHandler(raw).load("gbk.properties");
System.out.println("默认编码 app.title = " + raw.getString("app.title"));
System.out.println("默认编码 app.owner = " + raw.getString("app.owner"));
// 显式指定 GBK 读取,中文恢复正常
FileBasedConfigurationBuilder<PropertiesConfiguration> b =
new FileBasedConfigurationBuilder<>(PropertiesConfiguration.class)
.configure(new Parameters().properties()
.setFileName("gbk.properties")
.setEncoding("GBK"));
System.out.println("GBK 读取 app.title = " + b.getConfiguration().getString("app.title"));
System.out.println("GBK 读取 app.owner = " + b.getConfiguration().getString("app.owner"));
}
}
回显(实测):
默认编码 app.title = ��������
默认编码 app.owner = ��άƽ̨��
GBK 读取 app.title = 订单服务
GBK 读取 app.owner = 运维平台组
XML 侧同理:XMLConfiguration 保存时若未设置编码,将按平台默认字符集写出。实测给 builder 加 .setEncoding("UTF-8") 前后对比——未设置时,源文件里的中文注释 数据库连接配置,由 XmlDemo 生成 在回写后变成(od -c 查看文件字节,实测):
< ! - - ? ? ? ? ? ? ? ? ? X m l D e m o ? ?
设置 .setEncoding("UTF-8") 后回写正常(实测文件内容首行):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 数据库连接配置,由 XmlDemo 生成 --><database>
结论:properties 记得 setEncoding(默认 ISO-8859-1),XML 读写都建议显式 setEncoding("UTF-8")。
五、异常策略参数
setThrowExceptionOnMissing(true) 把「静默返回 null」改为「抛 NoSuchElementException」,适合配置错误应尽早暴露的场景;为 false(默认)时 getString("not.exist") 返回 null,带默认值重载则返回默认值。实测效果见第三章 ParamDemo 输出第 5 行:
strict 模式抛出异常 -> Key 'not.exist' does not map to an existing object.
组合配置(CombinedConfiguration)同样支持该参数;getInt/getBoolean 等类型化方法在缺 key 时的行为受同一参数控制。
六、列表分隔符参数
2.x 的列表分隔策略由 ListDelimiterHandler 控制,默认是 DisabledListDelimiterHandler(完全禁用切分)——这与 1.x「加载即按逗号切分」的行为不同,也是从 1.x 迁移最常踩的坑之一。
package com.example;
import org.apache.commons.configuration2.MapConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DelimDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("delim.properties", "UTF-8")) {
out.println("v1=a\\,b,c");
out.println("v2=x,y,z");
out.println("url=https://quzuo.net/page\\,with\\,commas");
}
// 1) 2.x 默认 DisabledListDelimiterHandler:不切分,v2 读成整串单值
PropertiesConfiguration plain = new PropertiesConfiguration();
new FileHandler(plain).load("delim.properties");
System.out.println("== 默认(DisabledListDelimiterHandler,禁用切分) ==");
System.out.println("v2 size = " + plain.getList("v2").size()
+ " value = " + plain.getList("v2"));
// 2) 显式启用 DefaultListDelimiterHandler(','):逗号切分,\, 表示字面逗号
PropertiesConfiguration split = new PropertiesConfiguration();
split.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
new FileHandler(split).load("delim.properties");
List<Object> v2 = split.getList("v2");
System.out.println("== DefaultListDelimiterHandler(',') ==");
System.out.println("v2 = " + v2 + " (size=" + v2.size() + ")");
System.out.println("v1 = " + split.getList("v1"));
System.out.println("url= " + split.getString("url"));
// 3) 内存配置上同样生效
Map<String, Object> m = new HashMap<>();
m.put("tags", "java, config, library");
MapConfiguration mc = new MapConfiguration(m);
mc.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
List<Object> tags = mc.getList("tags");
System.out.println("== MapConfiguration 同一分隔器 ==");
System.out.println("tags = " + tags + " (size=" + tags.size() + ")");
}
}
回显(实测):
== 默认(DisabledListDelimiterHandler,禁用切分) ==
v2 size = 1 value = [x,y,z]
== DefaultListDelimiterHandler(',') ==
v2 = [x, y, z] (size=3)
v1 = [a,b, c]
url= https://quzuo.net/page,with,commas
== MapConfiguration 同一分隔器 ==
tags = [java, config, library] (size=3)
逐项解说:
- 默认策略下
v2=x,y,z读作一个字符串x,y,z; - 启用
DefaultListDelimiterHandler(',')后,未转义的逗号切分:v2变 3 个值;\,转义为字面逗号:v1变 2 个值a,b与c,url保持完整; LegacyListDelimiterHandler(javap实测存在于convert包)用于兼容 1.x 生成的历史 properties 文件——1.x 的转义规则更宽松(反斜杠双写转义、控制字符转义),仅在读旧文件时使用,源码注释明确「其他情况下不应使用该类」。
七、include 指令控制参数
properties 文件支持 include=其他文件 指令,生产环境这可能成为配置注入点。setIncludesAllowed(false) 关闭该能力(见第三章 ParamDemo 输出第 7 行,配置仍正常读取本文件内容)。该参数仅对 PropertiesConfiguration 族有效(PropertiesBuilderParametersImpl 专属,javap 实测)。
八、XML 专属参数
XML 参数族(javap 实测 XMLBuilderParametersImpl):setValidating(boolean)(DTD 校验)、setSchemaValidation(boolean)(XSD 校验)、setEntityResolver(EntityResolver)(实体解析,防 XXE 常配)、setDocumentBuilder(DocumentBuilder)、setPublicID/setSystemID、setExpressionEngine。
setExpressionEngine 决定键的语法。2.x 默认表达式引擎的键从根元素下一级开始写(与 1.x 带根元素前缀不同):根元素为 <database> 时读 url 而不是 database.url。切换为 XPath 引擎可获得条件查询能力,但 XPathExpressionEngine 依赖可选包 commons-jxpath——缺失时报错(实测):
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/jxpath/ri/model/NodePointerFactory
at com.example.XPathDemo.main(XPathDemo.java:23)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.jxpath.ri.model.NodePointerFactory
补入 commons-jxpath-1.4.0.jar 后运行(XPathDemo.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 org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
import java.io.PrintWriter;
public class XPathDemo {
public static void main(String[] args) throws Exception {
try (PrintWriter out = new PrintWriter("servers.xml", "UTF-8")) {
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.println("<servers>");
out.println(" <server name=\"db1\" status=\"up\"/>");
out.println(" <server name=\"db2\" status=\"down\"/>");
out.println("</servers>");
}
FileBasedConfigurationBuilder<XMLConfiguration> builder =
new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
.configure(new Parameters().xml()
.setFileName("servers.xml")
.setExpressionEngine(new XPathExpressionEngine()));
XMLConfiguration xml = builder.getConfiguration();
System.out.println("xpath up server = "
+ xml.getString("server[@name='db1']/@status"));
System.out.println("xpath all names = " + xml.getList("server/@name"));
}
}
回显(实测):
xpath up server = up
xpath all names = [db1, db2]
九、变量插值参数与 ${key} 语法
${key} 是 Commons Configuration 官方的变量插值语法(非模板占位符):加载取值时会把 ${key} 替换为对应 key 的实际值。规则实测如下:
- 无前缀
${key}:只在当前配置自身的 key 中解析,链式引用(${db.home}引用${path})会递归展开; - 带前缀
${prefix:variable}:走注册的 Lookup,内建支持${sys:系统属性名}与${env:环境变量名}(还支持 const/date/file/url 等,来自 commons-text 的 StringLookupFactory); - 解析失败:原样保留整段插值表达式(如引用不存在的
${nobody}),不报错; - 自定义前缀:
setPrefixLookups(Map.of("前缀", Lookup 实例))注册(见第三章 ParamDemo 第 4 段,secret:前缀做 base64 解码)。
完整演示(InterpDemo.java):
package com.example;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.MapConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.io.FileHandler;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class InterpDemo {
public static void main(String[] args) throws Exception {
// app.properties 中 db.home 引用同文件的 path,db.dir 再引用 db.home —— 这是
// Configuration 官方的变量插值语法 ${key},加载时会替换为对应 key 的实际值
try (PrintWriter out = new PrintWriter("interpolation.properties", "UTF-8")) {
out.println("path=/data/app");
out.println("db.home=${path}");
out.println("db.dir=${db.home}/mysql");
out.println("java.version.check=${java.version}");
out.println("whoami=${env:HOME}");
out.println("machine=${sys:user.name}@${env:HOSTNAME}");
}
PropertiesConfiguration props = new PropertiesConfiguration();
FileHandler handler = new FileHandler(props);
handler.load("interpolation.properties");
Configuration cfg = props;
System.out.println("path = " + cfg.getString("path"));
System.out.println("db.home = " + cfg.getString("db.home"));
System.out.println("db.dir = " + cfg.getString("db.dir"));
System.out.println("java.version.check= " + cfg.getString("java.version.check"));
System.out.println("whoami = " + cfg.getString("whoami"));
System.out.println("machine = " + cfg.getString("machine"));
// 缺失的插值变量:默认返回原样字符串
Map<String, Object> m = new HashMap<>();
m.put("greeting", "hello, ${nobody}!");
Configuration mcfg = new MapConfiguration(m);
System.out.println("unresolved = " + mcfg.getString("greeting"));
}
}
回显(实测):
path = /data/app
db.home = /data/app
db.dir = /data/app/mysql
java.version.check= ${java.version}
whoami = /root
machine = root@a7048deace2c
unresolved = hello, ${nobody}!
重点解读:
db.dir=${db.home}/mysql输出/data/app/mysql:链式插值生效;java.version.check=${java.version}没有被解析成 JVM 版本——无前缀引用不会自动落到系统属性,要用${sys:java.version};${env:HOME}/${sys:user.name}/${env:HOSTNAME}输出真实环境值(/root、root、主机名);引用不存在的${nobody}原样保留。
十、自动重载参数
重载由三个角色协作:
- 检测器
ReloadingDetector:判断「需要重载」;文件型默认FileHandlerReloadingDetector,按最后修改时间判断; - 控制器
ReloadingController:汇聚检测结果,触发重建事件; - 触发器
PeriodicReloadingTrigger:后台线程按固定周期驱动控制器。
Builder 侧对应参数:setReloadingRefreshDelay(Long)(两次检测间的最小间隔)、setReloadingDetectorFactory(ReloadingDetectorFactory)(自定义检测器)。实践上最省事的是直接使用 ReloadingFileBasedConfigurationBuilder——它自带检测器与控制器装配(javap 实测该类实现 ReloadingControllerSupport)。
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();
}
}
回显(实测):
first read thread.pool.size = 4
after edit thread.pool.size = 16
new key max.queue = 1000
要点:进程未重启、代码未手动 load(),外部修改后新 key 也随之出现。注意重载生效后 getConfiguration() 返回的是新对象,业务代码应每次通过 builder 获取而不是长期持有旧引用(示例中的轮询正是这么写的)。用完记得 trigger.stop() 释放后台线程。
十一、组合配置与覆盖顺序
CombinedConfiguration 把多个配置源叠成一棵逻辑树,覆盖顺序由 Combiner 决定:
OverrideCombiner:先加入者优先——读值时从前面的源开始找,找到即返回;UnionCombiner:取所有源的并集,同 key 时取第一个源的值。
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"));
}
}
回显(实测):
== 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
逐项解读:
db.url:override 层先加入 → 覆盖 base 的 localhost 值,读到 prod-db;feature.b:override 里有true→ 覆盖 base 的false;db.user/feature.a/app.nameoverride 缺失 → 回落到 base;UnionCombiner同 key 取先加入者:db.url读到 base 的 localhost;- 「命令行 > 配置文件」只需在运行时把命令行来源的
MapConfiguration/SystemConfiguration第一个addConfiguration。
addConfiguration(配置, "名字") 的第二个参数给子配置命名,之后可用 getConfiguration("名字") 单独取回某一层。
十二、参考链接
- Parameters 工厂 Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/builder/fluent/Parameters.html
- ReloadingFileBasedConfigurationBuilder Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/builder/ReloadingFileBasedConfigurationBuilder.html
- CombinedConfiguration Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/CombinedConfiguration.html
- ListDelimiterHandler Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/convert/ListDelimiterHandler.html
- 用户指南·Howto Basic Features(插值与异常策略):https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html
- 用户指南·Howto Reloading:https://commons.apache.org/proper/commons-configuration/userguide/howto_reloading.html