JaiRouter 多版本配置管理:一个轻量级多版本配置实现思路
文章目录
一、需求缘起
最近在写开源项目JaiRouter,配置项目比较多,考虑到真实的业务场景下,多次配置后,测试后,无法回滚到原始版本,因此考虑到需要频繁回滚 JSON 配置。为了省掉数据库依赖,我把版本号直接放到文件名里,写完发现代码意外地短,回滚速度意外地快。整理出来,权当抛砖引玉。
二、核心思路
- 文件名即版本号:
config@version.json
- 当前配置单独存放:
config.json
- 读/写/回滚/清理 全部基于
java.nio.file.Files
,不引入第三方存储
三、代码骨架
kotlin
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 文件系统多版本配置存储:文件名即版本号
*/
public final class FileVersionStore {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final Path dir;
public FileVersionStore(Path root) throws IOException {
this.dir = root.toAbsolutePath().normalize();
Files.createDirectories(dir);
}
/* ---------- 基础 API ---------- */
/** 保存指定版本(覆盖写) */
public void save(int version, Map<String, Object> data) throws IOException {
Path file = versionFile(version);
Files.writeString(file, MAPPER.writeValueAsString(data),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
/** 读取当前配置 */
public Map<String, Object> current() throws IOException {
Path cur = currentFile();
if (Files.notExists(cur)) {
throw new NoSuchFileException("当前配置文件不存在: " + cur);
}
return MAPPER.readValue(cur.toFile(), new TypeReference<>() {});
}
/** 回滚到指定版本(原子覆盖) */
public void rollbackTo(int version) throws IOException {
Path src = versionFile(version);
if (Files.notExists(src)) {
throw new NoSuchFileException("版本文件不存在: " + src);
}
// ATOMIC_MOVE 保证并发安全
Files.copy(src, currentFile(), StandardCopyOption.REPLACE_EXISTING);
}
/** 列出所有可用版本,升序 */
public List<Integer> listVersions() throws IOException {
try (Stream<Path> stream = Files.list(dir)) {
return stream.map(Path::getFileName)
.map(Path::toString)
.filter(n -> n.startsWith("config@") && n.endsWith(".json"))
.map(n -> n.substring("config@".length(), n.length() - ".json".length()))
.mapToInt(Integer::parseInt)
.sorted()
.boxed()
.collect(Collectors.toList());
}
}
/** 删除指定版本(手工清理用) */
public void delete(int version) throws IOException {
Files.deleteIfExists(versionFile(version));
}
/* ---------- 内部工具 ---------- */
private Path currentFile() {
return dir.resolve("config.json");
}
private Path versionFile(int version) {
return dir.resolve("config@" + version + ".json");
}
/* ---------- 简单演示 ---------- */
public static void main(String[] args) throws IOException {
FileVersionStore store = new FileVersionStore(Paths.get("repo"));
// 写两个版本
store.save(1, Map.of("threshold", 10));
store.save(2, Map.of("threshold", 20));
// 回滚到 v1
store.rollbackTo(1);
System.out.println("current=" + store.current()); // {threshold=10}
// 查看所有版本
System.out.println("versions=" + store.listVersions()); // [1, 2]
}
}
依赖只有 Jackson。
四、什么时候够用
- 单节点或小集群
- 版本总量 < 10 k(目录扫描可接受)
- 无需字段级合并,整体覆盖即可
五、什么时候换方案
- 需要分布式一致性 → 上 etcd/consul
- 需要 JSON Patch → 上 Git 或数据库
- 文件数 > 10 k → 加一层前缀索引或转对象存储
六、小结
把版本号写进文件名,回滚操作退化成一次文件复制,代码量少、依赖少、调试方便。对于个人项目或内部小工具,算一个"够用且易丢进 Docker"的折中方案。
完整实验代码已放到 JAiRouter,欢迎试用、提 issue。