
引言:为什么配置管理如此重要?
在软件开发中,配置管理是一个看似简单却极易出错的话题。从数据库连接字符串到功能开关,从超时时间到日志级别,配置遍布应用的每个角落。然而,传统的配置方案往往存在以下痛点:
- Properties 文件:扁平结构,无法表达嵌套关系
- XML 配置:冗长繁琐,可读性差
- YAML:缩进敏感,容易因格式问题导致错误
- 硬编码:缺乏灵活性,环境切换困难
Typesafe Config 的出现彻底改变了这一局面。作为 JVM 生态中最流行的配置库,它不仅解决了上述问题,还带来了类型安全、不可变性、变量替换等高级特性。本文将带你从基础 API 到高级用法,全面掌握 Typesafe Config 的精髓。
1. 初识 Typesafe Config
Typesafe Config 是由 Lightbend 公司(Scala 语言的发明者)开发的一款纯 Java 配置库,具有以下核心特性:
| 特性 | 说明 |
|---|---|
| 零依赖 | 纯 Java 实现,不依赖任何第三方库 |
| 多格式支持 | Properties、JSON、HOCON(Human-Optimized Config Object Notation) |
| 类型安全 | 强类型 API,编译期发现配置错误 |
| 不可变性 | 所有配置对象不可变,线程安全 |
| 变量替换 | 支持 ${variable} 语法进行配置引用 |
| 环境感知 | 自动整合系统属性和环境变量 |
2. HOCON:为程序员而生的配置格式
HOCON(Human-Optimized Config Object Notation)是 Typesafe Config 的灵魂所在。它是 JSON 的超集,但比 JSON 更友好、更灵活。
2.1 从 JSON 到 HOCON 的演进
让我们看一个配置文件的"进化史":
传统 JSON(冗余繁琐):
json
{
"database": {
"host": "localhost",
"port": 5432,
"username": "admin",
"password": "secret",
"pool": {
"min": 5,
"max": 20,
"timeout": "30s"
}
}
}
HOCON 版本(简洁优雅):
hocon
database {
host = localhost
port = 5432
username = admin
password = secret
pool {
min = 5
max = 20
timeout = 30s
}
}
更简洁的点符号版本:
hocon
database.host = localhost
database.port = 5432
database.username = admin
database.password = secret
database.pool.min = 5
database.pool.max = 20
database.pool.timeout = 30s
2.2 HOCON 的核心语法特性
2.2.1 注释支持
hocon
# 这是单行注释
// 这也是单行注释
database {
host = localhost // 行内注释
}
2.2.2 省略逗号和引号
hocon
server {
host = localhost # 不需要引号(无特殊字符时)
port = 8080 # 不需要逗号(换行即可分隔)
enabled = true # 布尔值
}
2.2.3 变量替换与继承
hocon
# 定义基础配置
defaults {
timeout = 30s
retries = 3
}
# 继承并覆盖
http-client = ${defaults} {
timeout = 60s # 覆盖 timeout,保留 retries = 3
}
grpc-client = ${defaults} {
retries = 5 # 覆盖 retries,保留 timeout = 30s
}
2.2.4 环境变量支持
hocon
database {
host = ${DB_HOST} # 从环境变量读取
password = ${?DB_PASSWORD} # ? 表示可选,不存在时不报错
}
2.2.5 数组操作
hocon
path = ["/usr/bin"]
path += "/usr/local/bin" # 追加元素
path += "/opt/bin"
2.2.6 多行字符串
hocon
sql.query = """
SELECT u.id, u.name, u.email
FROM users u
WHERE u.status = 'active'
ORDER BY u.created_at DESC
"""
3. Typesafe Config 入门
3.1 引入依赖
xml
<!-- Maven -->
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>1.4.3</version>
</dependency>
3.2 第一个示例
java
public class ConfigQuickStart {
public static void main(String[] args) {
// 加载配置(默认加载 application.conf)
Config config = ConfigFactory.load();
// 基本类型读取
String host = config.getString("database.host");
int port = config.getInt("database.port");
System.out.println("host: " + host + ", port: " + port);
// 使用嵌套配置读取
Config dbConfig = config.getConfig("database");
String host2 = dbConfig.getString("host");
System.out.println( "host2: " + host2);
// 或者直接使用路径
String poolMax = config.getString("database.pool.max");
System.out.println( "poolMax: " + poolMax);
// 读取列表
List<String> tables = config.getStringList("database.tables");
System.out.println( "tables: " + tables);
}
}
对应的 application.conf:
hocon
database {
host = localhost
port = 5432
username = admin
password = secret
pool {
min = 5
max = 20
timeout = 30s
}
tables = [t_a, t_b]
}
4. Typesafe Config 实践
4.1 配置加载
ConfigFactory 是 Typesafe Config 的入口类,提供了丰富的静态方法来创建和加载配置。
4.1.1 标准加载模式
Typesafe Config 的 ConfigFactory.load() 方法按如下优先级加载配置(后加载的覆盖先加载的):
- 系统属性(-Dkey=value)
- application.conf(类路径中所有以此命名的资源)
- application.json(类路径中所有以此命名的资源)
- application.properties(类路径中所有以此命名的资源)
- reference.conf(类路径中所有以此命名的资源)
java
// 默认加载:默认加载 application.conf
Config config = ConfigFactory.load();
int port = config.getInt("database.port");
System.out.println("port: " + port);
理解配置加载的优先级对于构建灵活的应用至关重要
此外还可以允许应用程序提供自定义的 Config 文件来替代默认配置,以应对应用需要在单个 JVM 内管理多个配置,或希望从其他位置加载额外配置文件的情况。可以使用 ConfigFactory.load("my_application.conf") 来加载自己的 my_application.conf 配置:
java
// 2. 自定义加载:加载指定名称的配置文件
Config customConfig = ConfigFactory.load("my_application.conf");
int port2 = customConfig.getInt("database.port");
System.out.println("port2: " + port2);
4.1.2 动态构建配置
从字符串解析(最灵活):
java
// HOCON 格式
String hoconConfigStr = "database = { host = \"localhost\", user = \"admin\" }";
Config hoconConfig = ConfigFactory.parseString(hoconConfigStr);
// JSON 格式
String jsonConfigStr = "{\"database\":{\"port\":8080}}";
Config jsonConfig = ConfigFactory.parseString(jsonConfigStr);
从 Map 构建:
java
Map<String, Object> map = new HashMap<>();
map.put("database.port", 8080);
map.put("database.host", "localhost");
map.put("tables", Arrays.asList("t_a", "t_b"));
Config mapConfig = ConfigFactory.parseMap(map);
从 Properties 构建:
java
Properties props = new Properties();
props.setProperty("database.port", "8080");
props.setProperty("database.host", "localhost");
Config propsConfig = ConfigFactory.parseProperties(props);
从文件系统加载:
java
Config config = ConfigFactory.parseFile(new File("/opt/config/test.conf"));
从类路径根目录下的配置:
java
// 加载 src/main/resources/my_application.conf
Config config = ConfigFactory.parseResources("my_application.conf").resolve();
// 指定 ClassLoader 加载资源,适用于 OSGi、多模块、插件化架构
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Config config1 = ConfigFactory.parseResources(classLoader, "my_application.conf");
从URL加载:
java
// 从本地文件 URL 加载,也可以指定从 HTTP URL 加载配置
URL url = null;
try {
url = new URL("file:///opt/config/test.conf");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
Config config = ConfigFactory.parseURL(url);
4.1.3 获取默认配置层
java
// 系统属性(-Dkey=value,最高优先级)
Config overrides = ConfigFactory.defaultOverrides();
// 应用配置(application.conf)
Config appConfig = ConfigFactory.defaultApplication();
// 参考配置(reference.conf,库的默认值)
Config reference = ConfigFactory.defaultReference();
// 环境变量
Config env = ConfigFactory.systemEnvironment();
// 空配置(用于构建)
Config empty = ConfigFactory.empty();
4.2 读取配置的 N 种方式
4.2.1 基本类型读取
Typesafe Config 提供了丰富的类型特定方法,这是其 "Typesafe" 之名的由来:
java
public static void getPrimitives(Config config) {
// 字符串
String host = config.getString("database.host");
System.out.println("host: " + host);
// 整数(自动类型转换)
int port = config.getInt("database.port");
System.out.println("port: " + port);
long max = config.getLong("database.pool.max");
System.out.println("max: " + max);
double ratio = config.getDouble("database.pool.ratio");
System.out.println("ratio: " + ratio);
// 布尔值(支持多种格式)
boolean enabled = config.getBoolean("database.enabled");
// 支持:true/false, yes/no, on/off
System.out.println("enabled: " + enabled);
// 枚举类型
TimeUnit unit = config.getEnum(TimeUnit.class, "database.pool.unit");
System.out.println("unit: " + unit);
}
对应的配置文件:
hocon
database {
enabled=true
host=localhost
pool {
max=3600
ratio=0.5
unit=SECONDS
}
port=8080
tables=[
"t_a",
"t_b"
]
}
4.2.2 特殊类型:Duration 和 MemorySize
这是 Typesafe Config 的杀手级特性,让配置更加语义化:
java
public static void getSpecialTypes(Config config) {
// Duration 类型:支持人性化的时间格式
Duration timeout = config.getDuration("database.pool.timeout");
// 配置文件中可以写:"10s", "5m", "1h", "2d", "500ms"
System.out.println("timeout: " + timeout);
// 获取特定单位
long expire = config.getDuration("database.pool.expire", TimeUnit.MILLISECONDS);
System.out.println("expire: " + expire);
// MemorySize 类型:支持人性化的存储单位
ConfigMemorySize cacheSize = config.getMemorySize("database.cache.maxSize");
// 配置文件中可以写:"512k", "10m", "1g", "2t"
long bytes = cacheSize.toBytes();
long bytesDirect = config.getBytes("database.cache.maxSize");
System.out.println("bytes: " + bytes);
System.out.println("bytesDirect: " + bytesDirect);
}
对应的配置文件:
hocon
database {
cache {
maxSize="512k"
}
pool {
expire=1776522287
timeout="30s"
}
}
4.2.3 列表和嵌套对象
java
public static void getComplexTypes(Config config) {
// 字符串列表
List<String> tables = config.getStringList("database.tables");
System.out.println("tables: " + tables);
// 整数列表
List<Integer> ports = config.getIntList("server.ports");
System.out.println("ports: " + ports);
// Duration 列表
List<Duration> intervals = config.getDurationList("retry.intervals");
System.out.println("intervals: " + intervals);
}
对应的配置文件:
hocon
database {
tables=[
"t_a",
"t_b"
]
}
retry {
intervals=[
"10s",
"15s"
]
}
server {
ports=[
8080,
8081
]
}
4.2.4 路径检查与默认值
java
// 检查路径是否存在
if (config.hasPath("optional.config")) {
String value = config.getString("optional.config");
}
// 检查是否为 null
if (!config.getIsNull("maybe.null")) {
String value = config.getString("maybe.null");
}
// 使用 Optional 模式
Optional<String> value = config.hasPath("key")
? Optional.of(config.getString("key"))
: Optional.empty();
4.3 配置修改与操作
4.3.1 不可变性
Typesafe Config 的核心设计原则之一是 不可变性。所有修改操作都返回新的 Config 对象,原对象保持不变:
java
Config original = ConfigFactory.parseString("port = 8080");
// withValue 返回新对象,original 不变
Config modified = original.withValue("port", ConfigValueFactory.fromAnyRef(9090));
System.out.println(original.getInt("port")); // 8080
System.out.println(modified.getInt("port")); // 9090
这种设计带来了巨大优势:
- 线程安全:无需同步,可在多线程间自由共享
- 可预测性:不会出现意外的副作用
- 函数式风格:支持链式操作
4.3.2 withValue:精准设置单个值
定义:
java
Config withValue(String path, ConfigValue value);
参数说明:
- path:配置路径,使用点号分隔,如 "app.name" 或 "database.pool.max"
- 设置对象路径会替换整个子树,不是合并
- 路径不存在时,自动创建中间路径(如设置 a.b.c 会自动创建 a 和 a.b)
- value:ConfigValue 类型,通过 ConfigValueFactory 创建
可以使用 withValue 修改/添加单个配置值:
java
String str = "server { host = \"localhost\", port = 8080}";
Config config = ConfigFactory.parseString(str);
Config newConfig = config.withValue("server.port", ConfigValueFactory.fromAnyRef(9090));
可以使用 withValue 修改/添加多个配置值:
java
String str = "server { host = \"localhost\", port = 8080}";
Config config = ConfigFactory.parseString(str);
Map<String, Object> updates = Maps.newHashMap();
updates.put("server.host", "127.0.0.1");
updates.put("server.port", 1111);
updates.put("server.enabled", false);
Config newConfig = config;
for (Map.Entry<String, Object> entry : updates.entrySet()) {
newConfig = newConfig.withValue(entry.getKey(), ConfigValueFactory.fromAnyRef(entry.getValue()));
}
4.3.3 withFallback:智能配置合并
这是 Typesafe Config 最强大的功能之一,用于构建配置优先级链:
java
// 各层配置
Config config1 = ConfigFactory.parseString("{k1=1}");
Config config2 = ConfigFactory.parseString("{k1=11,k2=2}");
Config config3 = ConfigFactory.parseString("{k1=12,k2=21,k3=3}");
// 合并:高优先级在前,config1 > config2 > config3
Config finalConfig = config1.withFallback(config2).withFallback(config3);
// k1=1, k2=2, k3=3
System.out.println("k1=" + finalConfig.getString("k1") + ", k2=" + finalConfig.getString("k2") + ", k3=" + finalConfig.getString("k3"));
你也可以通过实现修改/添加配置值的能力:
java
Map<String, Object> objects = Maps.newHashMap();
objects.put("server.host", "127.0.0.1");
objects.put("server.port", 1111);
objects.put("server.enabled", false);
// 将更新转为 Config,优先级更高
Config overrides = ConfigFactory.parseMap(objects);
Config newConfig = overrides.withFallback(config).resolve();
withValue vs withFallback 的关键区别是什么呢?
- withValue 会完全替换路径的值(小心丢失数据)
- withFallback 会递归合并(保留所有配置,高优先级覆盖低优先级)
| 方法 | 核心作用 | 使用场景 |
|---|---|---|
| withValue | 精准替换单个路径的值 | 修改/添加特定配置项 |
| withFallback | 智能合并两个配置对象 | 构建配置优先级链 |
java
Config base = ConfigFactory.parseString("""
app {
name = "MyApp"
port = 8080
}
""");
// withValue:完全替换路径
Config withValue = base.withValue(
"app",
ConfigValueFactory.fromMap(Map.of("debug", true))
);
// 结果:只有 app.debug,app.name 和 app.port 丢失了!
// withFallback:递归合并
Config fallback = ConfigFactory.parseString("app { debug = true }");
Config withFallback = fallback.withFallback(base);
// 结果:app.name = MyApp, app.port = 8080, app.debug = true
4.3.4 withoutPath:删除配置项
可以使用 withoutPath 删除单个配置值:
java
String str = "server { host = \"localhost\", port = 8080, enabled = true }";
Config config = ConfigFactory.parseString(str);
Config newConfig = config.withoutPath("server.enabled");
可以使用 withoutPath 批量删除多个配置项:
java
String str = "server { host = \"localhost\", port = 8080, enabled = true }";
Config config = ConfigFactory.parseString(str);
Config newConfig = config;
for (String path : paths) {
newConfig = newConfig.withoutPath(path);
}
4.4 变量替换与环境配置
4.4.1 变量替换语法
HOCON 格式支持强大的变量替换功能:
hocon
# 定义基础值
default-timeout = 30s
# 引用变量
http-timeout = ${default-timeout}
grpc-timeout = ${default-timeout}
# 变量拼接
base-url = "https://api.example.com"
users-url = ${base-url}"/users"
orders-url = ${base-url}"/orders"
# 可选变量(不存在时不报错)
optional-value = ${?MAYBE_SET}
# 自引用(追加)
path = "/usr/bin"
path = ${path}":/usr/local/bin"
4.4.2 环境变量集成
hocon
database {
host = "localhost"
host = ${?DB_HOST} # 环境变量覆盖,可选
port = 5432
port = ${?DB_PORT}
password = ${?DB_PASSWORD} # 敏感信息从环境变量读取
}
启动方式:
bash
# 方式1:系统属性
java -Ddatabase.host=prod.db.com -Ddatabase.port=5433 -jar app.jar
# 方式2:环境变量(需启用覆盖)
export DB_HOST=prod.db.com
export DB_PASSWORD=secret123
java -Dconfig.override_with_env_vars=true -jar app.jar
# 方式3:指定外部配置文件
java -Dconfig.file=/etc/myapp/production.conf -jar app.jar
4.4.3 resolve 解析变量
java
// parse 后必须 resolve 才能使用变量替换
Config unresolved = ConfigFactory.parseString("""
base = 10
derived = ${base}
""");
// 错误:会抛出异常
// int value = unresolved.getInt("derived");
// 正确:先 resolve
Config resolved = unresolved.resolve();
int value = resolved.getInt("derived"); // 10
// 带选项的 resolve
ConfigResolveOptions options = ConfigResolveOptions.defaults()
.setAllowUnresolved(true) // 允许未解析的变量
.setUseSystemEnvironment(true);
Config resolved = config.resolve(options);
4.5 配置输出
TypeSafe Config 的配置输出,简单来说就是将程序运行时加载的、经过合并和解析后的配置数据,以指定的格式(如 HOCON 或 JSON)导出成字符串。其核心方法是 Config 对象的 root().render()。root() 方法能让你获取配置的根节点,返回一个 ConfigObject。而 ConfigObject 的 render() 方法,则是最终将整个配置树序列化为字符串的关键:
java
Map<String, Object> map = new HashMap<>();
map.put("database.port", 8080);
map.put("database.host", "localhost");
map.put("tables", Arrays.asList("t_a", "t_b"));
Config config = ConfigFactory.parseMap(map);
// 渲染为字符串
String output = config.root().render();
输出结果如下所示:
{
# hardcoded value
"database" : {
# hardcoded value
"host" : "localhost",
# hardcoded value
"port" : 8080
},
# hardcoded value
"tables" : [
# hardcoded value
"t_a",
# hardcoded value
"t_b"
]
}
render() 接收一个 ConfigRenderOptions 参数,用来精确控制输出的格式和样式。ConfigRenderOptions 类提供了几个预设的选项,你也可以根据自己的需要进行定制:
| 选项 | 预设样式 | 说明 |
|---|---|---|
| concise() | 紧凑JSON格式 | 最小化输出,无空格、无注释,非常适合机器处理和日志记录 |
| defaults() | 默认HOCON格式 | 启用注释,但不做美化,保留一定的可读性。 |
| compact() | 无注释的紧凑JSON | 与 concise() 类似,主要用于向后兼容。 |
除了使用预设,你也可以通过 ConfigRenderOptions.defaults() 获得一个基础选项,然后使用链式调用进行自定义。
java
// 美观格式输出
String pretty = config.root().render(
ConfigRenderOptions.defaults()
.setFormatted(true) // 开启格式化(美化输出)
.setJson(false) // 输出 HOCON 格式(false为HOCON,true为JSON)
.setComments(false) // 去掉配置文件中手写的注释
.setOriginComments(false) // 关闭自动生成的"配置来源"注释
这些方法的主要作用是:
setFormatted(boolean): 决定是否对输出进行"美化"(添加换行和缩进),便于人类阅读。setJson(boolean): 控制输出格式。设为 true 输出标准 JSON,设为 false 则输出 HOCON 格式。setOriginComments(boolean): 控制是否在输出中包含# hardcoded value这类自动生成的"来源"注释。这类注释对于调试很有帮助,但在生产环境中通常建议关闭。setComments(boolean): 控制是否保留配置文件中原有的用户注释
输出结果如下所示:
database {
host=localhost
port=8080
}
tables=[
"t_a",
"t_b"
]
可以设置 setJson 为 true 输出 JSON 格式:
// 输出为 JSON
String json = config.root().render(
ConfigRenderOptions.defaults()
.setFormatted(true) // 开启格式化(美化输出)
.setJson(true) // 输出 JSON 格式(false为HOCON,true为JSON)
.setComments(false) // 去掉配置文件中手写的注释
.setOriginComments(false) // 关闭自动生成的"配置来源"注释
);
System.out.println(json);
输出结果如下所示:
json
{
"database" : {
"host" : "localhost",
"port" : 8080
},
"tables" : [
"t_a",
"t_b"
]
}
从上面可以看到输出都是多行,如果单行简洁格式输出,可以使用方式:
// 简洁格式(单行)
String concise = config.root().render(
ConfigRenderOptions.concise()
);
输出结果如下所示:
{"database":{"host":"localhost","port":8080},"tables":["t_a","t_b"]}