Apache Commons Configuration 快速入门与集成示例

以 2.15.1 实测 Apache Commons Configuration 快速入门:Maven 依赖接入、Configurations 一行式门面与自动保存、环境变量与系统属性源、组合配置优先级、不重启自动重载,示例输出双系统实测。

Apache Commons ConfigurationApache Commons Configuration2026/10/13

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 环境
JDKOpenJDK 17.0.20.1OpenJDK 17.0.1
Maven3.6.33.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 Configuration2Apache Commonsproperties/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") 直接拿到 int 20,字符串 "20" 自动转型,这是它比 java.util.Properties 顺手的地方
  • getString("not.exist", "default"):key 不存在时返回第二个参数的默认值;不带默认值的 getString("not.exist") 则返回 null
  • handler.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

三个要点:

  1. builder.save() 把 app.port 改成 8081 后,用全新的 configs.properties("app.properties") 重读,读到的是新值 8081
  2. setAutoSave(true) 之后连 save() 都不用调了,setProperty("app.name", "order-service-v2") 一执行磁盘立刻更新
  3. 对 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 系统属性SystemConfigurationjava -D 定义的属性名读系统数据部署期覆盖
OS 环境变量EnvironmentConfiguration环境变量名,区分大小写读系统数据部署平台注入变量
内存 MapMapConfigurationMap 的 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 秒检测周期 + 轮询间隔)。另注意重载期间文件必须始终可解析,写一半的文件会触发加载异常,生产上建议「写临时文件后原子改名」。

十、参考链接

  1. Apache Commons Configuration 官网:https://commons.apache.org/proper/commons-configuration/
  2. 2.x 用户指南·Quick start:https://commons.apache.org/proper/commons-configuration/userguide/quick_start.html
  3. 2.x 用户指南·Configuration builders(FileBasedConfigurationBuilder 与自动保存):https://commons.apache.org/proper/commons-configuration/userguide/howto_configurationbuilders.html
  4. 2.x Javadoc(Configurations/ReloadingFileBasedConfigurationBuilder/PeriodicReloadingTrigger):https://commons.apache.org/proper/commons-configuration/apidocs/index.html
  5. Maven 中央仓库 commons-configuration2 2.15.1:https://repo1.maven.org/maven2/org/apache/commons/commons-configuration2/2.15.1/