Java零组件实现配置热更新
在某些场景下,我们需要实现配置的热更新,但是又要实现软件即插即用的需求,这就使我们不能引入过多复杂的插件,而nacos等配置中心在分布式业务场景下对配置的管理起着很重要作用,为此需要想一些办法去代替它们而完成同样的功能。
我们知道,在Springboot中,加载配置文件有一个默认的优先级,即外部启动命令>配置文件>本地配置文件,而nacos是通过bootstrap实现外部配置文件预加载。一个道理,我们也可以通过创建一个本地的配置文件实现同样的效果。这里的bootstrap配置文件就相当于外部配置文件
在开始实现前我们需要了解为什么不直接对本地的配置文件进行修改,因为我们知道Springboot是有注解可以实现环境刷新的,这是因为首先项目配置文件默认不允许运行时修改内容,其次项目进行打包后会将状态包装进一个编译后的目录中。这些原因使我们不对主配置文件进行直接修改。
下面我将以我开源项目中实现配置刷新的方法为例进行讲解。
场景如下:当服务器读取到本地服务列表后,将对用户进行展示,并在配置文件中配置其中一个服务进行使用,当用户选择服务列表中另一个服务时,将读取外部创建的配置文件(第一次修改时创建)进行修改,将需要改变的配置信息复制到外部配置文件中,再执行刷新,此时当系统检测到外部配置文件后将自动优先读取外部配置文件信息。
这是实现逻辑代码:
@PostMapping("/switchmodel")
@ResponseBody
public ResponseEntity<String> updateModel(@RequestBody Model model) throws IOException {System.out.println("执行到修改模型方法");// 修改配置文件// 获取项目根目录路径Path configPath = Paths.get(System.getProperty("user.dir"),"config","application.properties").normalize().toAbsolutePath();System.out.println("操作配置文件路径:" + configPath);// 如果外部配置文件不存在则创建if (!Files.exists(configPath)) {Files.createDirectories(configPath.getParent());try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("application.properties");) {Files.copy(inputStream, configPath);}}//读取当前项目配置文件List<String> lines = Files.readAllLines(configPath);//将配置文件信息进行修改,匹配需要改动的地方进行更新lines = lines.stream().map(line -> line.startsWith("spring.ai.ollama.chat.model") ?"spring.ai.ollama.chat.model=" + model.getModelName() : line).collect(Collectors.toList());//将更新后的配置信息写入外部配置文件try (BufferedWriter writer = Files.newBufferedWriter(configPath)) {writer.write(String.join("\n", lines));}List<String> writtenLines = Files.readAllLines(configPath);System.out.println("新的配置信息:" + writtenLines);//执行刷新modelMessageService.asyncRefreshConfig1();return ResponseEntity.ok("切换成功");}
然后执行刷新,刷新方法如下
@Autowiredprivate ContextRefresher contextRefresher;//刷新配置public void asyncRefreshConfig1() {CompletableFuture.runAsync(() -> {contextRefresher.refresh();}, Executors.newCachedThreadPool()); // 使用独立线程池}
这里采用独立线程池,有这些考虑:首先就是利用多线程优化执行效率,还有就是避免发生死锁,因为有时配置的刷新直接服务于项目中用到的某个对象,该对象会依赖于配置文件的属性,由于是单例模式,我们不能重复创建同一个bean,所以要采用注解实现更新,当在并发场景下或项目中配置了多个该注解发挥作用时,由于服务于同一个业务,可能会会发生争抢资源的问题,所以采用独立线程的原因就是这个。
所以我们也提到了,如果配置服务于某个bean,那么就需要在bean定义的类上加上@RefreshScope注解以便能够进行状态刷新。