欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot中配置屬性熱更新的輕量級實(shí)現(xiàn)方案

 更新時(shí)間:2025年07月20日 08:06:52   作者:風(fēng)象南  
項(xiàng)目開發(fā)中,每次修改配置都要重啟服務(wù),不僅開發(fā)效率低,線上重啟還會導(dǎo)致短暫不可用,今天分享一個(gè)輕量級方案,基于SpringBoot原生能力實(shí)現(xiàn)配置熱更新,有需要的可以了解下

項(xiàng)目開發(fā)中,每次修改配置(比如調(diào)整接口超時(shí)時(shí)間、限流閾值)都要重啟服務(wù),不僅開發(fā)效率低,線上重啟還會導(dǎo)致短暫不可用。

雖然Spring Cloud Config、Apollo這類配置中心能解決問題,但對于中小項(xiàng)目來說太重了——要部署服務(wù),成本太高。

今天分享一個(gè)輕量級方案,基于SpringBoot原生能力實(shí)現(xiàn)配置熱更新,不用額外依賴,代碼量不到200行。

一、為什么需要“輕量級”熱更新?

先說說傳統(tǒng)配置方案的痛點(diǎn)

痛點(diǎn)1:改配置必須重啟服務(wù)

開發(fā)環(huán)境中,改個(gè)日志級別都要重啟服務(wù),浪費(fèi)時(shí)間;生產(chǎn)環(huán)境更麻煩,重啟會導(dǎo)致流量中斷,影響用戶體驗(yàn)。

痛點(diǎn)2:重量級配置中心成本高

Spring Cloud Config、Apollo功能強(qiáng)大,但需要單獨(dú)部署服務(wù)、維護(hù)元數(shù)據(jù),小項(xiàng)目用不上這么復(fù)雜的功能,純屬“殺雞用牛刀”。

痛點(diǎn)3:@Value注解不支持動(dòng)態(tài)刷新

即使通過@ConfigurationProperties綁定配置,默認(rèn)也不會自動(dòng)刷新,必須結(jié)合@RefreshScope,但@RefreshScope會導(dǎo)致Bean重建,可能引發(fā)狀態(tài)丟失。

我們需要什么?

  • 無需額外依賴,基于SpringBoot原生API
  • 支持properties/yaml文件熱更新
  • 不重啟服務(wù),修改配置后自動(dòng)生效
  • 對業(yè)務(wù)代碼侵入小,改造成本低

二、核心原理:3個(gè)關(guān)鍵技術(shù)點(diǎn)

輕量級熱更新的實(shí)現(xiàn)依賴SpringBoot的3個(gè)原生能力,不需要引入任何第三方框架

2.1 配置文件監(jiān)聽:WatchService

Java NIO提供的WatchService可以監(jiān)聽文件系統(tǒng)變化,當(dāng)配置文件(如application.yml)被修改時(shí),能觸發(fā)回調(diào)事件。

2.2 屬性刷新:Environment與ConfigurationProperties

Spring的Environment對象存儲了所有配置屬性,通過反射更新其內(nèi)部的PropertySources,可以實(shí)現(xiàn)配置值的動(dòng)態(tài)替換。
同時(shí),@ConfigurationProperties綁定的Bean需要重新綁定屬性,這一步可以通過ConfigurationPropertiesBindingPostProcessor實(shí)現(xiàn)。

2.3 事件通知:ApplicationEvent

自定義一個(gè)ConfigRefreshEvent事件,當(dāng)配置更新后發(fā)布事件,業(yè)務(wù)代碼可以通過@EventListener接收通知,處理特殊邏輯(如重新初始化連接池)。

三、手把手實(shí)現(xiàn):不到200行代碼

3.1 第一步:監(jiān)聽配置文件變化

創(chuàng)建ConfigFileWatcher類,使用WatchService監(jiān)聽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 {
    // 監(jiān)聽的配置文件路徑(默認(rèn)監(jiān)聽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毫秒防抖時(shí)間

    // 注入配置刷新處理器(后面實(shí)現(xiàn))
    public ConfigFileWatcher(ConfigRefreshHandler refreshHandler) {
        this.refreshHandler = refreshHandler;
    }

    @PostConstruct
    public void init() throws IOException {
        // 獲取配置文件的實(shí)際路徑
        Resource resource = new FileSystemResource(ResourceUtils.getFile(configPath));
        Path configDir = resource.getFile().toPath().getParent(); // 監(jiān)聽配置文件所在目錄
        String fileName = resource.getFilename(); // 配置文件名(如application.yaml)

        watchService = FileSystems.getDefault().newWatchService();
        // 注冊文件修改事件(ENTRY_MODIFY)
        configDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        // 啟動(dòng)線程監(jiān)聽文件變化
        executor.submit(() -> {
            while (true) {
                try {
                    WatchKey key = watchService.take(); // 阻塞等待事件
                    // 防抖檢查:忽略短時(shí)間內(nèi)重復(fù)事件
                    if (System.currentTimeMillis() - lastProcessTime < EVENT_DEBOUNCE_TIME) {
                        continue;
                    }
                    for (WatchEvent<?> event : key.pollEvents()) {
                        WatchEvent.Kind<?> kind = event.kind();
                        if (kind == StandardWatchEventKinds.OVERFLOW) {
                            continue; // 事件溢出,忽略
                        }

                        // 檢查是否是目標(biāo)配置文件被修改
                        Path changedFile = (Path) event.context();
                        if (changedFile.getFileName().toString().equals(fileName)) {
                            log.info("檢測到配置文件修改:{}", fileName);
                            refreshHandler.refresh(); // 觸發(fā)配置刷新
                        }
                    }
                    boolean valid = key.reset(); // 重置監(jiān)聽器
                    if (!valid) break; // 監(jiān)聽器失效,退出循環(huán)
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        log.info("配置文件監(jiān)聽器啟動(dòng)成功,監(jiān)聽路徑:{}", configDir);
    }

    @PreDestroy
    public void destroy() {
        executor.shutdownNow();
        try {
            watchService.close();
        } catch (IOException e) {
            log.error("關(guān)閉WatchService失敗", e);
        }
    }
}

3.2 第二步:實(shí)現(xiàn)配置刷新邏輯

創(chuàng)建ConfigRefreshHandler類,核心功能是更新Environment中的屬性,并通知@ConfigurationProperties Bean刷新

import org.springframework.context.ApplicationEvent;
import java.util.Set;

/**
 * 自定義配置刷新事件
 */
public class ConfigRefreshedEvent extends ApplicationEvent {
    // 存儲變化的配置鍵(可選,方便業(yè)務(wù)判斷哪些配置變了)
    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. 重新讀取配置文件內(nèi)容
            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);
        }
    }

    // 讀取配置文件內(nèi)容(支持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)) {
                    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);
        for (String beanName : beanNames) {
            // 重新綁定屬性(關(guān)鍵:不重建Bean,只更新屬性值)
            bindingPostProcessor.postProcessBeforeInitialization(
                    applicationContext.getBean(beanName), beanName);
            log.info("刷新配置Bean:{}", beanName);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

3.3 第三步:注冊監(jiān)聽器Bean

在SpringBoot配置類中注冊ConfigFileWatcher,使其隨應(yīng)用啟動(dòng)

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綁定屬性

創(chuàng)建業(yè)務(wù)配置類,用@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; // 默認(rèn)超時(shí)時(shí)間3秒
    private int maxRetries = 2; // 默認(rèn)重試次數(shù)2次
}

3.5 第五步:測試熱更新效果

創(chuàng)建測試Controller,驗(yàn)證配置修改后是否自動(dòng)生效

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; // 返回當(dāng)前配置
    }

    // 監(jiān)聽配置刷新事件,可進(jìn)行業(yè)務(wù)特殊處理
    @EventListener(ConfigRefreshedEvent.class)
    public void appConfigUpdate(ConfigRefreshedEvent event) {
        event.getChangedKeys().forEach(key -> log.info("配置項(xiàng) {} 發(fā)生變化", key));
    }

}

四、生產(chǎn)環(huán)境使用

問題1:使用外部配置文件

解決方案:配置文件外置通過環(huán)境變量或啟動(dòng)參數(shù)指定外部路徑,結(jié)合ConfigFileWatcher監(jiān)聽外部配置文件

// 修改ConfigFileWatcher的init方法
@PostConstruct
public void init() throws IOException {
    // 生產(chǎn)環(huán)境建議監(jiān)聽外部配置文件(如/opt/app/application.yml)
    Path configPath = Paths.get("/opt/app/application.yml");
    if (Files.exists(configPath)) {
        watchConfigFile(configPath); // 監(jiān)聽外部文件
    } else {
        log.warn("外部配置文件不存在,使用默認(rèn)配置");
    }
}

private void watchConfigFile(Path configPath) throws IOException {
    Path configDir = configPath.getParent();
    String fileName = configPath.getFileName().toString();
    // 后續(xù)邏輯同上...
}

問題2:敏感配置解密

解決方案:結(jié)合Jasypt實(shí)現(xiàn)配置在loadConfigFile中解密

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

五、總結(jié)

輕量級配置熱更新方案的核心是“利用SpringBoot原生能力+最小化改造”,適合中小項(xiàng)目或需要快速集成的場景。相比重量級配置中心,它的優(yōu)勢在于:

零依賴:無需部署額外服務(wù),代碼量少

低成本:對現(xiàn)有項(xiàng)目侵入小,改造成本低

易維護(hù):基于Spring原生API,無需學(xué)習(xí)新框架

到此這篇關(guān)于SpringBoot中配置屬性熱更新的輕量級實(shí)現(xiàn)方案的文章就介紹到這了,更多相關(guān)SpringBoot配置屬性熱更新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Boot2+JPA之悲觀鎖和樂觀鎖實(shí)戰(zhàn)教程

    Spring Boot2+JPA之悲觀鎖和樂觀鎖實(shí)戰(zhàn)教程

    這篇文章主要介紹了Spring Boot2+JPA之悲觀鎖和樂觀鎖實(shí)戰(zhàn)教程,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • 解析Spring中@Controller@Service等線程安全問題

    解析Spring中@Controller@Service等線程安全問題

    這篇文章主要為大家介紹解析了Spring中@Controller@Service等線程的安全問題,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-03-03
  • 基于springboot bean的實(shí)例化過程和屬性注入過程

    基于springboot bean的實(shí)例化過程和屬性注入過程

    這篇文章主要介紹了基于springboot bean的實(shí)例化過程和屬性注入過程,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Spring?Boot實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的解決方案

    Spring?Boot實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的解決方案

    文章介紹了在SpringBoot中實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的幾種方案,并詳細(xì)描述了一個(gè)使用AbstractRoutingDataSource的實(shí)現(xiàn)步驟,感興趣的朋友一起看看吧
    2025-01-01
  • 通過實(shí)例講解springboot整合WebSocket

    通過實(shí)例講解springboot整合WebSocket

    這篇文章主要介紹了通過實(shí)例講解springboot整合WebSocket,WebSocket為游覽器和服務(wù)器提供了雙工異步通信的功能,即游覽器可以向服務(wù)器發(fā)送消息,服務(wù)器也可以向游覽器發(fā)送消息。,需要的朋友可以參考下
    2019-06-06
  • 基于Java和XxlCrawler獲取各城市月度天氣情況實(shí)踐分享

    基于Java和XxlCrawler獲取各城市月度天氣情況實(shí)踐分享

    本文主要講解使用Java開發(fā)語言,使用XxlCrawler框架進(jìn)行智能的某城市月度天氣抓取實(shí)踐開發(fā),文章首先介紹目標(biāo)網(wǎng)站的相關(guān)頁面及目標(biāo)數(shù)據(jù)的元素,然后講解在信息獲取過程的一些參數(shù)配置以及問題應(yīng)對,需要的朋友可以參考下
    2024-05-05
  • spring-boot整合Micrometer+Prometheus的詳細(xì)過程

    spring-boot整合Micrometer+Prometheus的詳細(xì)過程

    這篇文章主要介紹了springboot整合Micrometer+Prometheus的詳細(xì)過程,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2024-05-05
  • Springboot項(xiàng)目Maven依賴沖突的問題解決

    Springboot項(xiàng)目Maven依賴沖突的問題解決

    使用Spring Boot和Maven進(jìn)行項(xiàng)目開發(fā)時(shí),依賴沖突是一個(gè)常見的問題,本文就來介紹一下Springboot項(xiàng)目Maven依賴沖突的問題解決,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-07-07
  • 微信支付java版本之查詢訂單

    微信支付java版本之查詢訂單

    這篇文章主要為大家詳細(xì)介紹了微信支付java版本之查詢訂單,為大家分享了微信支付訂單的查詢接口,感興趣的小伙伴們可以參考一下
    2016-08-08
  • httpclient staleConnectionCheckEnabled獲取連接流程解析

    httpclient staleConnectionCheckEnabled獲取連接流程解析

    這篇文章主要為大家介紹了httpclient staleConnectionCheckEnabled獲取連接流程示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11

最新評論