watchservice热加载json配置需监听目录并处理entry_create/modify/delete事件,校验文件名、长度及延时落盘,用volatile不可变容器安全替换配置,避免并发与编辑器兼容性问题。

Java里用WatchService监听JSON配置文件变化
热加载的前提是能及时感知文件改动,WatchService是JDK原生方案,比轮询省资源、比第三方库轻量。但它不递归监听子目录,也不自动处理文件重命名/移动这类“伪修改”,实际用时得兜底。
常见错误现象:StandardWatchEventKinds.ENTRY_MODIFY事件可能在编辑器保存时触发多次(比如先清空再写入),或根本没触发(因为某些编辑器用“写新文件+原子替换”方式保存,触发的是ENTRY_DELETE和ENTRY_CREATE)。
- 监听路径必须是目录,不能直接监听单个
config.json;把配置文件放在conf/下,监听该目录 - 注册时传
StandardWatchEventKinds.ENTRY_CREATE、ENTRY_MODIFY、ENTRY_DELETE三个事件,覆盖主流编辑行为 - 事件回调里要校验
watchEvent.context()是否等于目标文件名,避免误响应其他文件 - Windows下对符号链接支持弱,Linux/macOS更稳定
解析JSON时避免NullPointerException和重复解析
监听到变化后立刻读文件、解析,但磁盘写入可能还没完成,或者多个事件连发导致并发解析同一文件——这时容易遇到空内容、半截JSON或JsonParseException。
关键不是“快”,而是“稳”。解析前加一层简单校验,比强行捕获异常更可控。
立即学习“Java免费学习笔记(深入)”;
- 用
Files.size(path)确认文件非零长度,跳过0字节事件(常见于编辑器临时写入) - 加毫秒级延时(如
Thread.sleep(50)),等编辑器真正落盘;不要用File.lastModified()做判断,精度低且不可靠 - 解析逻辑包在
synchronized块或用ReentrantLock保护,防止多事件并发触发导致配置对象状态混乱 - 推荐用
com.fasterxml.jackson.databind.ObjectMapper而非org.json,前者对空值/注释容忍度更高,且支持@JsonIgnoreProperties(ignoreUnknown = true)
热加载后如何安全替换运行时配置对象
旧配置对象还在被线程使用,新对象一上来就直接赋值,很可能引发ConcurrentModificationException或读到部分更新的状态。Java里没有“原子替换引用”的魔法,得靠设计约束访问路径。
最简方案是把配置对象包装成不可变容器,所有外部读取都走统一入口。
- 定义一个
ConfigHolder类,内部用volatile Config current存储当前实例 - 热加载成功后,构建全新
Config对象(确保构造过程无副作用),再用current = newConfig赋值 - 业务代码一律调用
ConfigHolder.get().getTimeout(),而不是持有旧引用 - 避免在
Config类里放可变集合(如ArrayList),改用Collections.unmodifiableList()封装
Spring Boot项目里别硬套原生WatchService
如果你的项目已经用了Spring Boot,WatchService反而容易和@ConfigurationProperties、@RefreshScope冲突。Spring Cloud Config或spring-boot-devtools的restart机制更适合开发期,但生产环境要谨慎。
真实生产场景中,多数团队会退回到“手动触发刷新”或“定时拉取+比对MD5”,因为文件系统监听在容器化部署(尤其是K8s挂载ConfigMap)下表现不稳定。
- Spring Boot 2.4+默认禁用
devtools的文件监听,需显式配置spring.devtools.restart.enabled=true -
@RefreshScope要求Bean是代理对象,普通工具类或静态配置无法生效 - ConfigMap热更新本质是挂载为volume,文件系统事件不可靠,建议改用
kubectl rollout restart或结合Consul/Etcd做配置中心
热加载真正的难点不在监听或解析,而在于你怎么让整个应用接受“配置变了”这个事实——线程池大小、数据库连接超时、HTTP客户端重试策略……这些都不是改个字段就能生效的。得一个个看它们是否支持运行时调整,否则监听再准也没用。










