SpringBoot動態更新yml文件

来源:https://www.cnblogs.com/yanpeng19940119/archive/2022/12/31/17017146.html
-Advertisement-
Play Games

前言 在系統運行過程中,可能由於一些配置項的簡單變動需要重新打包啟停項目,這對於在運行中的項目會造成數據丟失,客戶操作無響應等情況發生,針對這類情況對開發框架進行升級提供yml文件實時修改更新功能 項目依賴 項目基於的是2.0.0.RELEASE版本,所以snakeyaml需要單獨引入,高版本已包含 ...


前言

在系統運行過程中,可能由於一些配置項的簡單變動需要重新打包啟停項目,這對於在運行中的項目會造成數據丟失,客戶操作無響應等情況發生,針對這類情況對開發框架進行升級提供yml文件實時修改更新功能

項目依賴

項目基於的是2.0.0.RELEASE版本,所以snakeyaml需要單獨引入,高版本已包含在內

        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.23</version>
        </dependency>

網上大多數方法是引入spring-cloud-context配置組件調用ContextRefresher的refresh方法達到同樣的效果,考慮以下兩點未使用

  • 開發框架使用了logback日誌,引入spring-cloud-context會造成日誌配置讀取錯誤
  • 引入spring-cloud-context會同時引入spring-boot-starter-actuator組件,會開放一些健康檢查路由及埠,需要對框架安全方面進行額外控制

YML文件內容獲取

讀取resource文件下的文件需要使用ClassPathResource獲取InputStream

    public String getTotalYamlFileContent() throws Exception {
        String fileName = "application.yml";
        return getYamlFileContent(fileName);
    }
    public String getYamlFileContent(String fileName) throws Exception {
        ClassPathResource classPathResource = new ClassPathResource(fileName);
        return onvertStreamToString(classPathResource.getInputStream());
    }
    public static String convertStreamToString(InputStream inputStream) throws Exception{
       return IOUtils.toString(inputStream, "utf-8");
    }

YML文件內容更新

我們獲取到yml文件內容後可視化顯示到前臺進行展示修改,將修改後的內容通過yaml.load方法轉換成Map結構,再使用yaml.dumpAsMap轉換為流寫入到文件

    public void updateTotalYamlFileContent(String content) throws Exception {
        String fileName = "application.yml";
        updateYamlFileContent(fileName, content);
    }
	public void updateYamlFileContent(String fileName, String content) throws Exception {
        Yaml template = new Yaml();
        Map<String, Object> yamlMap = template.load(content);

        ClassPathResource classPathResource = new ClassPathResource(fileName);

        Yaml yaml = new Yaml();
        //字元輸出
        FileWriter fileWriter = new FileWriter(classPathResource.getFile());
        //用yaml方法把map結構格式化為yaml文件結構
        fileWriter.write(yaml.dumpAsMap(yamlMap));
        //刷新
        fileWriter.flush();
        //關閉流
        fileWriter.close();
    }

YML屬性刷新

yml屬性在程式中讀取使用一般有三種

  • 使用Value註解
    @Value("${system.systemName}")
    private String systemName;
  • 通過enviroment註入讀取
    @Autowired
    private Environment environment;
    
    environment.getProperty("system.systemName")
  • 使用ConfigurationProperties註解讀取
@Component
@ConfigurationProperties(prefix = "system")
public class SystemConfig {
    private String systemName;
}

Property刷新

我們通過environment.getProperty方法讀取的配置集合實際是存儲在PropertySources中的,我們只需要把鍵值對全部取出存儲在propertyMap中,將更新後的yml文件內容轉換成相同格式的ymlMap,兩個Map進行合併,調用PropertySources的replace方法進行整體替換即可

但是yaml.load後的ymlMap和PropertySources取出的propertyMap兩者數據解構是不同的,需要進行手動轉換

propertyMap集合就是單純的key,value鍵值對,key是properties形式的名稱,例如system.systemName=>xxxxx集團管理系統

ymlMap集合是key,LinkedHashMap的嵌套層次結構,例如system=>(systemName=>xxxxx集團管理系統)

  • 轉換方法如下
  public HashMap<String, Object> convertYmlMapToPropertyMap(Map<String, Object> yamlMap) {
        HashMap<String, Object> propertyMap = new HashMap<String, Object>();
        for (String key : yamlMap.keySet()) {
            String keyName = key;
            Object value = yamlMap.get(key);
            if (value != null && value.getClass() == LinkedHashMap.class) {
                convertYmlMapToPropertyMapSub(keyName, ((LinkedHashMap<String, Object>) value), propertyMap);
            } else {
                propertyMap.put(keyName, value);
            }
        }
        return propertyMap;
    }

    private void convertYmlMapToPropertyMapSub(String keyName, LinkedHashMap<String, Object> submMap, Map<String, Object> propertyMap) {
        for (String key : submMap.keySet()) {
            String newKey = keyName + "." + key;
            Object value = submMap.get(key);
            if (value != null && value.getClass() == LinkedHashMap.class) {
                convertYmlMapToPropertyMapSub(newKey, ((LinkedHashMap<String, Object>) value), propertyMap);
            } else {
                propertyMap.put(newKey, value);
            }
        }
    }
  • 刷新方法如下
        String name = "applicationConfig: [classpath:/" + fileName + "]";
        MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
        Map<String, Object> source = propertySource.getSource();
        Map<String, Object> map = new HashMap<>(source.size());
        map.putAll(source);

        Map<String, Object> propertyMap = convertYmlMapToPropertyMap(yamlMap);

        for (String key : propertyMap.keySet()) {
            Object value = propertyMap.get(key);
            map.put(key, value);
        }
        environment.getPropertySources().replace(name, new MapPropertySource(name, map));

註解刷新

不論是Value註解還是ConfigurationProperties註解,實際都是通過註入Bean對象的屬性方法使用的,我們先自定註解RefreshValue來修飾屬性所在Bean的class

通過實現InstantiationAwareBeanPostProcessorAdapter介面在系統啟動時過濾篩選對應的Bean存儲下來,在更新yml文件時通過spring的event通知更新對應

bean的屬性即可

  • 註冊事件使用EventListener註解
    @EventListener
    public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
        if(mapper.containsKey(configUpdateEvent.key)){
            List<FieldPair> fieldPairList = mapper.get(configUpdateEvent.key);
            if(fieldPairList.size()>0){
                for (FieldPair fieldPair:fieldPairList) {
                    fieldPair.updateValue(environment);
                }
            }
        }
    }
  • 通知觸發事件使用ApplicationContext的publishEvent方法
    @Autowired
    private ApplicationContext applicationContext;
    
  	for (String key : propertyMap.keySet()) {
       applicationContext.publishEvent(new YamlConfigRefreshPostProcessor.ConfigUpdateEvent(this, key));
    }

YamlConfigRefreshPostProcessor的完整代碼如下

@Component
public class YamlConfigRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
    private Map<String, List<FieldPair>> mapper = new HashMap<>();
    private Environment environment;

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        processMetaValue(bean);
        return super.postProcessAfterInstantiation(bean, beanName);
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    private void processMetaValue(Object bean) {
        Class clz = bean.getClass();
        if (!clz.isAnnotationPresent(RefreshValue.class)) {
            return;
        }

        if (clz.isAnnotationPresent(ConfigurationProperties.class)) {
            //@ConfigurationProperties註解
            ConfigurationProperties config = (ConfigurationProperties) clz.getAnnotation(ConfigurationProperties.class);
            for (Field field : clz.getDeclaredFields()) {
                String key = config.prefix() + "." + field.getName();
                if(mapper.containsKey(key)){
                    mapper.get(key).add(new FieldPair(bean, field, key));
                }else{
                    List<FieldPair> fieldPairList = new ArrayList<>();
                    fieldPairList.add(new FieldPair(bean, field, key));
                    mapper.put(key, fieldPairList);
                }
            }
        } else {
            //@Valuez註解
            try {
                for (Field field : clz.getDeclaredFields()) {
                    if (field.isAnnotationPresent(Value.class)) {
                        Value val = field.getAnnotation(Value.class);
                        String key = val.value().replace("${", "").replace("}", "");
                        if(mapper.containsKey(key)){
                            mapper.get(key).add(new FieldPair(bean, field, key));
                        }else{
                            List<FieldPair> fieldPairList = new ArrayList<>();
                            fieldPairList.add(new FieldPair(bean, field, key));
                            mapper.put(key, fieldPairList);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }
    }

    public static class FieldPair {
        private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",
                ":", true);
        private Object bean;
        private Field field;
        private String value;

        public FieldPair(Object bean, Field field, String value) {
            this.bean = bean;
            this.field = field;
            this.value = value;
        }

        public void updateValue(Environment environment) {
            boolean access = field.isAccessible();
            if (!access) {
                field.setAccessible(true);
            }
            try {
                if (field.getType() == String.class) {
                    String updateVal = environment.getProperty(value);
                    field.set(bean, updateVal);
                }
                else if (field.getType() == Integer.class) {
                    Integer updateVal = environment.getProperty(value,Integer.class);
                    field.set(bean, updateVal);
                }
                else if (field.getType() == int.class) {
                    int updateVal = environment.getProperty(value,int.class);
                    field.set(bean, updateVal);
                }
                else if (field.getType() == Boolean.class) {
                    Boolean updateVal = environment.getProperty(value,Boolean.class);
                    field.set(bean, updateVal);
                }
                else if (field.getType() == boolean.class) {
                    boolean updateVal = environment.getProperty(value,boolean.class);
                    field.set(bean, updateVal);
                }
                else {
                    String updateVal = environment.getProperty(value);
                    field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            field.setAccessible(access);
        }

        public Object getBean() {
            return bean;
        }

        public void setBean(Object bean) {
            this.bean = bean;
        }

        public Field getField() {
            return field;
        }

        public void setField(Field field) {
            this.field = field;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    public static class ConfigUpdateEvent extends ApplicationEvent {
        String key;

        public ConfigUpdateEvent(Object source, String key) {
            super(source);
            this.key = key;
        }
    }

    @EventListener
    public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
        if(mapper.containsKey(configUpdateEvent.key)){
            List<FieldPair> fieldPairList = mapper.get(configUpdateEvent.key);
            if(fieldPairList.size()>0){
                for (FieldPair fieldPair:fieldPairList) {
                    fieldPair.updateValue(environment);
                }
            }
        }
    }
}


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 mysql 報錯解決mysql> grant all on *.* to "dba"@"%" identified by "mysql123";ERROR 1819 (HY000): Your password does not satisfy the current policy requir ...
  • 開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第3天,點擊查看活動詳情 如果你正需要處理Flutter異常捕獲,那麼恭喜你,找對地了,這裡從根源上給你準備了Flutter異常捕獲需要是所有知識和原理,讓你更深刻認識Flutter Zone概念。 Zone是什麼 /// A zo ...
  • Vue3,webpack,vite 通用 適用於中大型項目中 1.安裝vuex npm i vuex 2.創建倉庫與文件結構(核心) 一,創建入口 在src目錄下創建store文件夾,store文件夾下創建 下麵文件結構 actions.js import * as type from './mut ...
  • 基於jQuery的三種AJAX請求 1. 介紹 get請求 通常用於 獲取服務端資源(向伺服器要資源) ​ 例如:根據URL地址,從伺服器獲取HTML文件、CSS文件、JS文件、圖片文件、數據資源等。 post請求 通常用於 向伺服器提交數據(往伺服器發送資源) ​ 例如:登錄時向伺服器提交的登錄信 ...
  • 1776 年亞當斯密發表《國富論》,標志著經濟學的誕生。2004 年,一本名為《領域驅動設計·軟體核心複雜性應對之道》的書問世,開闢了軟體開發的一個新流派:領域驅動設計。看完這本書,十個人有九個人的感覺都是:似懂非懂,若有所得,掩卷長思,一無所得,我個人的感覺同樣如此。出於興趣,多年來仔細研讀了幾十... ...
  • HttpClient 版本已經到 5.2.1 了. 在版本4中的一些方法已經變成 deprecated, 於是將之前的工具類升級一下, 順便把中間遇到的問題記錄一下 ...
  • 一、註冊流程 單nacos節點流程圖如下: 流程圖可以知,Nacos註冊流程包括客戶端的服務註冊、服務實例列表拉取、定時心跳任務;以及服務端的定時檢查服務實例任務、服務實例更新推送5個功能。 服務註冊:當客戶端啟動的時候會根據當前微服務的配置信息把微服務註冊到nacos服務端。 服務實例列表拉取:當 ...
  • 家居網購項目實現011 以下皆為部分代碼,詳見 https://github.com/liyuelian/furniture_mall.git 27.功能25-事務管理 27.1下訂單問題思考 在生成訂單的功能中,系統會去同時修改資料庫中的order,order_item,furn三張表,如果有任意 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...