Apache Commons Configuration 配置参数详解

解析 Apache Commons Configuration 2.15.1 参数体系:Parameters 路径、编码、异常策略、分隔符、include 控制、XML 表达式引擎、变量插值、自动重载与组合配置覆盖顺序,示例与输出全部实测。

Apache Commons ConfigurationApache Commons Configuration2026/10/13

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 类文件格式专属参数
propertiesparams.properties()PropertiesConfigurationsetIncludesAllowed、setIOFactory、setLayout
XMLparams.xml()XMLConfigurationsetValidating、setSchemaValidation、setExpressionEngine、setEntityResolver
INIparams.ini()INIConfiguration继承层级参数
JSONparams.json()JSONConfiguration继承层级参数(需 jackson-databind)
YAMLparams.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 的实际值。规则实测如下:

  1. 无前缀 ${key}:只在当前配置自身的 key 中解析,链式引用(${db.home} 引用 ${path})会递归展开;
  2. 带前缀 ${prefix:variable}:走注册的 Lookup,内建支持 ${sys:系统属性名} 与 ${env:环境变量名}(还支持 const/date/file/url 等,来自 commons-text 的 StringLookupFactory);
  3. 解析失败:原样保留整段插值表达式(如引用不存在的 ${nobody}),不报错;
  4. 自定义前缀: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} 原样保留。

十、自动重载参数

重载由三个角色协作:

  1. 检测器 ReloadingDetector:判断「需要重载」;文件型默认 FileHandlerReloadingDetector,按最后修改时间判断;
  2. 控制器 ReloadingController:汇聚检测结果,触发重建事件;
  3. 触发器 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.name override 缺失 → 回落到 base;
  • UnionCombiner 同 key 取先加入者:db.url 读到 base 的 localhost;
  • 「命令行 > 配置文件」只需在运行时把命令行来源的 MapConfiguration/SystemConfiguration 第一个 addConfiguration。

addConfiguration(配置, "名字") 的第二个参数给子配置命名,之后可用 getConfiguration("名字") 单独取回某一层。

十二、参考链接

  1. Parameters 工厂 Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/builder/fluent/Parameters.html
  2. ReloadingFileBasedConfigurationBuilder Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/builder/ReloadingFileBasedConfigurationBuilder.html
  3. CombinedConfiguration Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/CombinedConfiguration.html
  4. ListDelimiterHandler Javadoc:https://commons.apache.org/proper/commons-configuration/apidocs/org/apache/commons/configuration2/convert/ListDelimiterHandler.html
  5. 用户指南·Howto Basic Features(插值与异常策略):https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html
  6. 用户指南·Howto Reloading:https://commons.apache.org/proper/commons-configuration/userguide/howto_reloading.html