从 DataX 的配置加载说起:我用 FastJson2 做了一个轻量级动态配置管理库

一个真实的痛点

你有没有遇到过这样的场景?

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 是阿里巴巴开源的离线数据同步工具,它的配置加载流程是这样的:

  1. 解析 job.json(用户任务配置)
  2. 合并 core.json(全局默认配置)
  3. 合并 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");

简单的事情,不应该复杂。

相关推荐
小锋java12341 小时前
分享一套锋哥原创的SpringBoot4+Vue3宠物领养网站系统
java
Csvn3 小时前
Nginx 配置与运维管理 — 从安装到 SSL 反向代理
后端
mqcode4 小时前
若依框架做大了怎么办?多模块 Maven 拆分的完整指南
后端
用户40269244819084 小时前
CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清
前端·后端
考虑考虑4 小时前
Java实现hmacsha1加密算法
java·后端·java ee
掉鱼的猫5 小时前
Spring Boot → Solon 注解迁移实战指南:一张对照表说清楚
java·spring boot
程序边界5 小时前
lac_agent自愈链路上篇——crontab守护的那些坑与健康检查实战
后端
笨鸟飞不快5 小时前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
plainGeekDev5 小时前
广播接收器 → Flow + Lifecycle
android·java·kotlin