开发者

SpringBoot中配置属性热更新的轻量级实现方案

目录
  • 一、为什么需要“轻量级”热更新?
  • 二、核心原理:3个关键技术点
    • 2.1 配置文件监听:WatchService
    • 2.2 属性刷新:Environment与ConfigurationProperties
    • 2.3 事件通知:ApplicationEvent
  • 三、手把手实现:不到200行代码
    • 3.1 第一步:监听配置文件变化
    • 3.2 第二步:实现配置刷新逻辑
    • 3.3 第三步:注册监听器Bean
    • 3.4 第四步:使用@ConfigurationProperties绑定属性
    • 3.5 第五步:测试热更新效果
  • 四、生产环境使用
    • 问题1:使用外部配置文件
    • 问题2:敏感配置解密
  • 五、总结

    项目开发中,每次修改配置(比如调整接口超时时间、限流阈值)都要重启服务,不仅开发效率低,线上重启还会导致短暂不可用。

    虽然Spring Cloud Config、Apollo这类配置中心能解决问题,但对于中小项目来说太重了——要部署服务,成本太高。

    今天分享一个轻量级方案,基于SpringBoot原生能力实现配置热更新,不用额外依赖,代码量不到200行。

    一、为什么需要“轻量级”热更新?

    先说说传统配置方案的痛点

    痛点1:改配置必须重启服务

    开发环境中,改个日志级别都要重启服务,浪费时间;生产环境更麻烦,重启会导致流量中断,影响用户体验。

    痛点2:重量级配置中心成本高

    Spring Cloud Config、Apollo功能强大,但需要单独部署服务、维护元数据,小项目用不上这么复杂的功能,纯属“杀鸡用牛刀”。

    痛点3:@Value注解不支持动态刷新

    即使通过@ConfigurationProperties绑定配置,默认也不会自动刷新,必须结合@RefreshScope,但@RefreshScope会导致Bean重建,可能引发状态丢失。

    我们需要什么?

    • 无需额外依赖,基于SpringBoot原生API
    • 支持properties/yaml文件热更新
    • 不重启服务,修改配置后自动生效
    • 对业务代码侵入小,改造成本低

    二、核心原理:3个关键技术点

    轻量级热更新的实现依赖SpringBoot的3个原生能力,不需要引入任何第三方框架

    2.1 配置文件监听:WatchService

    Java NIO提供的WatchService可以监听文件系统变化,当配置文件(如application.yml)被修改时,能触发回调事件。

    2.2 属性刷新:Environment与ConfigurationProperties

    Spring的Environment对象存储了所有配置属性,通过反射更新其内部的PropertySources,可以实现配置值的动态替换。

    同时,@ConfigurationProperties绑定的Bean需要重新绑定属性,这一步可以通过ConfigurationPropertiesBindingPostProcessor实现。

    2.3 事件通知:ApplicationEvent

    自定义一个ConfigRefreshEvent事件,当配置更新后发布事件,业务代码可以通过@EventListener接收通知,处理特殊逻辑(如重新初始化连接池)。

    三、手把手实现:不到200行代码

    3.1 第一步:监听配置文件变化

    创建ConfigFileWatcher类,使用WatchService监听application.ymlapplication.properties的修改

    package com.example.config;
    
    import jakarta.annotation.PostConstruct;
    import jakarta.annotation.PreDestroy;
    import lombok.extern.slf4j.编程客栈Slf4j;
    import org.springframework.core.io.FileSystemResource;
    import org.springframework.core.io.Resource;
    import org.springframework.util.ResourceUtils;
    
    import java.io.IOException;
    import java.nio.file.*;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Slf4j
    public class ConfigFileWatcher {
        // 监听的配置文件路径(默认监听classpath下的application.yaml)
        private final String configPath = "classpath:application.yaml";
        private WatchService watchService;
        private final ExecutorService executor = Executors.newSingleThreadExecutor();
        private final ConfigRefreshHandler refreshHandler;
        private long lastProcessTime;
        private final long EVENT_DEBOUNCE_TIME = 500; // 500毫秒防抖时间
    
        // 注入配置刷新处理器(后面实现)
        public ConfigFileWatcher(ConfigRefreshHandler refreshHandler) {
            this.refreshHandler = refreshHandler;
        }
    
        @PostConstruct
        public void init() throws IOException {
            // 获取配置文件的实际路径
            Resource resource = new FileSystemResource(ResourceUtils.getFile(configPath));
            Path configDir = resource.getFile().toPath().getParent(); // 监听配置文件所在目录
            String fileName = resource.getFilename(); // 配置文件名(如application.yaml)
    
            watchService = FileSystems.getDefault().newWatchService();
            // 注册文件修改事件(ENTRY_MODIFY)
            configDir.register(watchService, StandardwatchEventKinds.ENTRY_MODIFY);
    
            // 启动线程监听文件变化
            executor.submit(() -> {
                while (true) {
                    try {
                        WatchKey key = watchService.take(); // 阻塞等待事件
                        // 防抖检查:忽略短时间内重复事件
                        if (System.currentTimeMillis() - lastProcessTime < EVENT_DEBOUNCE_TIME) {
                            continue;
                        }
                        for (WatchEvent<?> event : key.pollEvents()) {
                            WatchEvent.Kind<?> kind = event.kind();
                            if (kind == StandardWatchEventKinds.OVERFLOW) {
                                continue; // 事件溢出,忽略
                            }
    
                            // 检查是否是目标配置文件被修改
                            Path changedFile = (Path) event.context();
                            if (changedFile.getFileName().toString().equals(fileName)) {
                                log.info("检测到配置文件修改:{}", fileName);
                                refreshHandler.refresh(); // 触发配置刷新
                            }
                        }
                        boolean valid = key.reset(); // 重置监听器
                        if (!valid) break; // 监听器失效,退出循环
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            });
            log.info("配置文件监听器启动成功,监听路径:{}", configDir);
        }
    
        @PreDestroy
        public void destroy() {
            executor.shutdownNow();
            try {
                watchService.close();
            } catch (IOException e) {
                log.error("关闭WatchService失败", e);
            }
        }
    }
    

    3.2 第二步:实现配置刷新逻辑

    创建ConfigRefreshHandler类,核心功能是更新Environment中的属性,并通知@ConfigurationProperties Bean刷新

    import org.springframework.context.ApplicationEvent;
    import java.util.Set;
    
    /**
     * 自定义配置刷新事件
     */
    public class ConfigRefreshedEvent extends ApplicationEvent {
        // 存储变化的配置键(可选,方便业务判断哪些配置变了)
        private final Set<String> changedKeys;
    
        public ConfigRefreshedEvent(Object source, Set<String> changedKeys) {
            super(source);
            this.changedKeys = changedKeys;
        }
    
        // 获取变化的配置键
        public Set<String> getChangedKeys() {
            return changedKeys;
        }
    }
    
    
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
    import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.core.env.MapPropertySource;
    import org.springframework.core.env.PropertySource;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    import java.util.*;
    
    @Component
    @Slf4j
    public class ConfigRefreshHandler implements ApplicationContextAware {
        @Autowired
        private ConfigurableEnvironment environment;
        private ApplicationContext applicationContext;
    
        @Autowired
        private ConfigurationPropertiesBindingPostProcessor bindingPostProcessor; // 属性绑定工具
    
        // 刷新配置的核心方法
        public void refresh() {
            try {
                // 1. 重新读取配置文件内容
                Properties properties = loadConfigFile();
    
                // 2. 更新Environment中的属性
                Set<String> changeKeys = updateEnvironment(properties);
    
                // 3. 重新绑定所有@ConfigurationProperties Bean
                if (!changeKeys.isEmpty()) {
                    rebindConfigurationProperties();
                }
    
                applicationContext.publishEvent( new ConfigRefreshedEvent(this,changeKeys));
                log.info("配置文件刷新完成");
            } catch (Exception e) {
                log.error("配置文件刷新失败", e);
            }
        }
    
        // 读取配置文件内容(支持properties和yaml)
        private Properties loadConfigFile() throws IOException {
            // 使用Spring工具类读取classpath下的配置文件
            Resource resource = new ClassPathResource("application.yaml");
            YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
            yamlFactory.setResources(resource);
    
            // 获取解析后的Properties对象
            Properties properties = yamlFactory.getObject();
            if (properties == null) {
                throw new IOException("Failed to load configuration file");
            }
            return properties;
        }
    
        // 更新Environment中的属性,返回变化的配置键集合
        private Set<String> updateEnvironment(Properties properties) {
            String sourceName = "Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'";
            Set<String> changedKeys = new HashSet<>();
            PropertySource<?> appConfig = environment.getPropertySources().get(sourceName);
    
            if (appConfig instanceof MapPropertySource) {
                Map<String, Object> sourceMap = new HashMap<>(((MapPropertySource) appConfig).getSource());
    
                properties.forEach((k, v) -> {
                    String key = k.toString();
                    Object oldValue = sourceMap.get(key);
                    if (!Objects.equals(oldValue, v)) {
          http://www.devze.com              changedKeys.add(key);
                    }
                    sourceMap.put(key, v);
                });
    
                environment.getPropertySources().replace(sourceName, new MapPropertySource(sourceName, sourceMap));
            }
            return changedKeys;
        }
    
        // 重新绑定所有@ConfigurationProperties Bean
        private void rebindConfigurationProperties() {
            // 获取所有@ConfigurationProperties Bean的名称
            String[] beanNames = applicationContext.getBeanNamesForAnnotation(org.springframework.boot.context.properties.ConfigurationProperties.class);
        http://www.devze.com    for (String beanName : beanNames) {
       javascript         // 重新绑定属性(关键:不重建Bean,只更新属性值)
                bindingPostPrandroidocessor.postProcessBeforeInitialization(
                        applicationContext.getBean(beanName), beanName);
                log.info("刷新配置Bean:{}", beanName);
            }
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }
    

    3.3 第三步:注册监听器Bean

    在SpringBoot配置类中注册ConfigFileWatcher,使其随应用启动

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class HotRefreshConfig {
        @Bean
        public ConfigFileWatcher configFileWatcher(ConfigRefreshHandler refreshHandler) throws IOException {
            return new ConfigFileWatcher(refreshHandler);
        }
    }
    

    3.4 第四步:使用@ConfigurationProperties绑定属性

    创建业务配置类,用@ConfigurationProperties绑定配置,无需额外注解即可支持热更新

    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Data
    @Component
    @ConfigurationProperties(prefix = "app") // 绑定配置前缀
    public class AppConfig {
        private int timeout = 3000; // 默认超时时间3秒
        private int maxRetries = 2; // 默认重试次数2次
    }
    

    3.5 第五步:测试热更新效果

    创建测试Controller,验证配置修改后是否自动生效

    package com.example.controller;
    
    import com.example.AppConfig;
    import com.example.config.ConfigRefreshedEvent;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.event.EventListener;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @Slf4j
    public class ConfigController {
        @Autowired
        private AppConfig appConfig;
    
        @GetMapping("/config")
        public AppConfig getConfig() {
            return appConfig; // 返回当前配置
        }
    
        // 监听配置刷新事件,可进行业务特殊处理
        @EventListener(ConfigRefreshedEvent.class)
        public void appConfigUpdate(ConfigRefreshedEvent event) {
            event.getChangedKeys().forEach(key -> log.info("配置项 {} 发生变化", key));
        }
    
    }
    

    四、生产环境使用

    问题1:使用外部配置文件

    解决方案:配置文件外置通过环境变量或启动参数指定外部路径,结合ConfigFileWatcher监听外部配置文件

    // 修改ConfigFileWatcher的init方法
    @PostConstruct
    public void init() throws IOException {
        // 生产环境建议监听外部配置文件(如/opt/app/application.yml)
        Path configPath = Paths.get("/opt/app/application.yml");
        if (Files.exists(configPath)) {
            watchConfigFile(configPath); // 监听外部文件
        } else {
            log.warn("外部配置文件不存在,使用默认配置");
        }
    }
    
    private void watchConfigFile(Path configPath) throws IOException {
        Path configDir = configPath.getParent();
        String fileName = configPath.getFileName().toString();
        // 后续逻辑同上...
    }
    

    问题2:敏感配置解密

    解决方案:结合Jasypt实现配置在loadConfigFile中解密

    // 伪代码:解密配置
    private String decrypt(String value) {
        if (value.startsWith("ENC(")) {
            return jasyptEncryptor.decrypt(value.substring(4, value.length() - 1));
        }
        return value;
    }
    

    五、总结

    轻量级配置热更新方案的核心是“利用SpringBoot原生能力+最小化改造”,适合中小项目或需要快速集成的场景。相比重量级配置中心,它的优势在于:

    零依赖:无需部署额外服务,代码量少

    低成本:对现有项目侵入小,改造成本低

    易维护:基于Spring原生API,无需学习新框架

    到此这篇关于SpringBoot中配置属性热更新的轻量级实现方案的文章就介绍到这了,更多相关SpringBoot配置属性热更新内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜