一个真实的痛点
你有没有遇到过这样的场景?
ini
// 从配置中心拉到一份 JSON 配置
String json = configCenter.get("app-config");
// 想取一个嵌套字段,层层 getJSONObject
JSONObject config = JSON.parseObject(json);
String host = config.getJSONObject("database").getString("host");
int port = config.getJSONObject("database").getIntValue("port");
// 想覆盖部分配置?手写 merge 逻辑,每次都不一样
// 想查一个条件?比如"所有价格低于 10 的商品",只能遍历数组手写过滤
更难受的是配置合并------本地默认配置、环境变量配置、远程配置,三层叠加,每次都要写一遍 merge 逻辑,还容易出 bug。
这个痛点,其实阿里巴巴的 DataX 早就遇到过。
DataX 的解法
DataX 是阿里巴巴开源的离线数据同步工具,它的配置加载流程是这样的:
- 解析 job.json(用户任务配置)
- 合并 core.json(全局默认配置)
- 合并 plugin.json(插件配置)
三层配置逐层叠加,形成最终的运行配置。DataX 的 Configuration 类核心方法就是 merge:
kotlin
// DataX Configuration.java 源码
public Configuration merge(final Configuration another, boolean updateWhenConflict) {
Set<String> keys = another.getKeys();
for (final String key : keys) {
if (updateWhenConflict) {
this.set(key, another.get(key));
continue;
}
// 忽略策略:只有当前 Configuration 不存在的 key,才更新
boolean isCurrentExists = this.get(key) != null;
if (isCurrentExists) {
continue;
}
this.set(key, another.get(key));
}
return this;
}
DataX 把所有 key 打平成 a.b.c 的点分路径,用一个 Map 存储。合并时按策略(覆盖或忽略)逐 key 处理。
这个设计很好,但它绑死了 DataX 的体系 ------Configuration 类有 800+ 行,和 DataX 的异常体系、加密逻辑深度耦合。如果你的项目只需要"解析 + 合并 + 查询"三件事,引入 DataX 整个 common 包显然太重了。
DynJsonConf:提取精华,轻装上阵
于是我把 DataX 配置管理的核心思路提炼出来,基于 FastJson2 做了一个轻量级库------DynJsonConf。
它只做三件事
| 能力 | 说明 | DataX 对应 |
|---|---|---|
| 配置解析 | JSON 字符串/文件 → JSONObject | Configuration.from() |
| 配置合并 | 浅合并,顶层 key 覆盖 | Configuration.merge() |
| JSONPath 查询 | 一行代码取任意嵌套值 | DataX 用 get("a.b.c") 打平路径 |
30 秒上手
vbscript
// 创建配置
DynJsonConf conf = new DynJsonConf("{"database":{"host":"localhost","port":3306},"timeout":30}");
// JSONPath 查询,告别层层 getJSONObject
String host = conf.getString("$.database.host"); // "localhost"
int port = conf.getInt("$.database.port", 0); // 3306
// 配置合并(浅合并:新值覆盖旧值,未涉及的 key 保留)
conf.merge("{"timeout":60,"retries":3}");
conf.getInt("$.timeout", 0); // 60(被覆盖)
conf.getInt("$.retries", 0); // 3 (新增)
conf.getString("$.database.host"); // "localhost"(保留)
对比 DataX:为什么选择 JSONPath
DataX 用 a.b.c 打平路径来查询配置:
ini
// DataX 的方式
String host = configuration.getString("job.content.reader.parameter.host");
DynJsonConf 直接用 FastJson2 的 JSONPath:
ini
// DynJsonConf 的方式
String host = conf.getString("$.job.content.reader.parameter.host");
看起来差不多?但 JSONPath 的能力远不止点号访问:
ini
// 过滤:价格低于 10 的商品
List<Object> cheap = conf.getList("$..book[?(@.price < 10)]");
// 深度扫描:任意层级的所有 author
List<String> authors = conf.getList("$..author");
// 条件查询:含有 isbn 字段的书
List<Object> withIsbn = conf.getList("$..book[?(@.isbn)]");
// 数组切片:前两本书
List<Object> firstTwo = conf.getList("$.store.book[0:2]");
// 数组大小
int count = conf.size("$.store.book");
这些查询用 DataX 的打平路径方式几乎无法实现,而 JSONPath 一行搞定。
对比 DataX:合并策略更清晰
DataX 的 merge 支持两种策略:updateWhenConflict=true(覆盖)和 false(忽略)。这在实际使用中容易混淆------你需要在每次调用时决定策略。
DynJsonConf 采用浅合并语义,规则简单明确:
- 新配置的顶层 key → 覆盖旧值
- 嵌套对象 → 整体替换(不深度合并)
- 数组 → 整体替换
- 新配置中没有的 key → 保留
arduino
conf.load("{"db":{"host":"localhost","port":3306},"timeout":30}");
conf.merge("{"db":{"host":"prod.example.com"},"retries":3}");
// 结果:
// db = {"host":"prod.example.com"} ← 嵌套对象整体替换,port 丢失
// timeout = 30 ← 保留
// retries = 3 ← 新增
这和多环境配置叠加的场景天然契合:默认配置 → 环境配置 → 运行时覆盖,逐层覆盖,逻辑清晰。
适用场景
1. 多环境配置叠加
java
// 加载默认配置
DynJsonConf conf = new DynJsonConf();
conf.loadFile("config/default.json");
// 叠加环境配置(开发/测试/生产)
conf.loadFile("config/" + env + ".json");
// 叠加远程配置中心的热更新
conf.merge(configCenter.fetch());
2. 规则引擎的条件查询
ini
// 查询所有满足条件的规则
List<Object> activeRules = conf.getList("$.rules[?(@.enabled == true)]");
// 查询优先级大于 5 的规则
List<Object> highPriority = conf.getList("$.rules[?(@.priority > 5)]");
3. 动态路由配置
ini
// 查找指向特定服务的路由
List<Object> routes = conf.getList("$.routes[?(@.service == 'order-service')]");
// 查找所有超时时间大于 3s 的下游
List<Object> slowDownstreams = conf.getList("$.downstreams[?(@.timeout > 3000)]");
4. 微服务配置热更新
以 Nacos 为例,DynJsonConf 负责内存中的配置替换,Nacos 负责监听变更并推送:
typescript
// 启动时加载初始配置
DynJsonConf conf = new DynJsonConf();
String initConfig = nacosConfigService.getConfig("app-config", "DEFAULT_GROUP", 5000);
conf.load(initConfig);
// 注册监听器,配置变更时自动合并
nacosConfigService.addListener("app-config", "DEFAULT_GROUP", new Listener() {
@Override
public void receiveConfigInfo(String newConfigJson) {
conf.merge(newConfigJson); // 线程安全,合并即生效
}
@Override
public Executor getExecutor() { return null; }
});
// 业务代码正常读取,下次调用即返回最新值
String dbHost = conf.getString("$.database.host");
注意:DynJsonConf 只负责内存中的配置替换。完整的热更新方案还需要配置中心配合,且部分运行时参数(如线程池大小)改了 JSON 后对应组件不会自动调整,需要额外的生效逻辑。
性能考量
JSONPath 表达式的编译有一定开销。DynJsonConf 内部使用 ConcurrentHashMap 缓存编译后的 JSONPath 对象,同一路径表达式的重复查询直接复用:
vbscript
// 第一次调用:编译 + 缓存
conf.getString("$.store.book[0].title");
// 后续调用:直接从缓存取编译结果
conf.getString("$.store.book[0].title");
读操作基于 volatile 引用,无锁;写操作(load/merge)使用 synchronized 保证原子性。在典型的"读多写少"配置场景下,性能表现良好。
和 DataX 的关系
| 维度 | DataX Configuration | DynJsonConf |
|---|---|---|
| 定位 | DataX 专属配置体系 | 通用轻量级配置库 |
| 依赖 | DataX common 全量包 | 仅 FastJson2 |
| 查询 | 打平路径 a.b.c |
JSONPath $.a.b.c |
| 合并 | 覆盖/忽略双策略 | 浅合并(语义明确) |
| 线程安全 | 无 | volatile 读 + synchronized 写 |
| 代码量 | 800+ 行(含加密等) | ~300 行核心代码 |
DynJsonConf 的灵感来源于 DataX 的配置加载思路------分层解析、逐层合并,但做了减法:去掉 DataX 业务耦合,换上 JSONPath 这个更强的查询引擎,让它成为任何 Java 项目都能直接使用的工具。
快速引入
Maven 依赖:
xml
<dependency>
<groupId>com.dynjsonconf</groupId>
<artifactId>dynjsonconf</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.61</version>
</dependency>
项目地址:github.com/yxss1010/Dy...
总结
如果你在项目中遇到过这些情况:
- JSON 配置层层嵌套,取值要写一串
getJSONObject - 多份配置需要合并,每次手写 merge 逻辑
- 需要按条件查询配置项,只能遍历数组手写过滤
- 配置需要热更新,但担心并发安全
试试 DynJsonConf,三行代码就能解决:
ini
DynJsonConf conf = new DynJsonConf();
conf.loadFile("config.json");
String value = conf.getString("$.deep.nested.key");
简单的事情,不应该复杂。