SpringBoot動(dòng)態(tài)更新外部屬性文件

摘要

  • 本文內(nèi)容基于springboot2.2.6
  • SpringBoot可以通過@PropertySource(value = "file:demo.properties")的方式加載外部配置文件朱盐,這樣打好jar包后只要將這個(gè)屬性文件放到相同路徑即可
  • 如果能夠在不重啟服務(wù)的情況下就可以重新加載這個(gè)屬性文件绷蹲,就可以很方便的實(shí)現(xiàn)動(dòng)態(tài)更新或渤,那么要怎么做呢?
  • github:https://github.com/hanqunfeng/springbootchapter/tree/master/chapter27

思路

SpringCloud可以通過config組件實(shí)現(xiàn)配置的動(dòng)態(tài)加載唾戚,我們也可以將數(shù)據(jù)存在數(shù)據(jù)庫或者緩存中卿操,可是如果只是一個(gè)小項(xiàng)目警检,不想依賴任何中間件,那么就可以通過如下的方式實(shí)現(xiàn)害淤。

  • 獲取所有注解了@PropertySource的對(duì)象扇雕,并且獲取其value屬性數(shù)組中是以file:開頭的文件路徑
  • 判斷是否同時(shí)注解了@ConfigurationProperties,并且獲取其prefix的值
  • 對(duì)每個(gè)屬性文件進(jìn)行遍歷窥摄,通過反射找到對(duì)象的field名稱(去除prefix后的名字)洼裤,并將屬性值賦值給該field

代碼

這個(gè)類要注冊(cè)到spring上下文,并在需要的地方調(diào)用該對(duì)象的refresh方法即可重新加載所有外部屬性文件溪王。

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

/**
 * <p>動(dòng)態(tài)加載外部屬性處理類</p>
 */
@Slf4j
@Component
public class ExternalPropertiesRefresh {

    @Autowired
    private ConfigurableListableBeanFactory configurableListableBeanFactory;

    /**
     * <p>根據(jù)屬性名獲取屬性值</p>
     *
     * @param fieldName bean的屬性名稱
     * @param object    bean對(duì)象
     * @return java.lang.Object get方法返回值
     * @author hanqf
     */
    private Object getFieldValueByName(String fieldName, Object object) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = object.getClass().getMethod(getter, new Class[]{});
            Object value = method.invoke(object, new Object[]{});
            return value;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }


    /**
     * <p>根據(jù)屬性名設(shè)置屬性值</p>
     *
     * @param fieldName  bean的屬性名稱
     * @param object     bean對(duì)象
     * @param paramTypes set方法參數(shù)類型
     * @param params     set方法參數(shù)值
     * @author hanqf
     */
    private void setFieldValueByName(String fieldName, Object object, Class[] paramTypes, Object[] params) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String setter = "set" + firstLetter + fieldName.substring(1);
            Method method = object.getClass().getMethod(setter, paramTypes);
            method.invoke(object, params);

        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }


    /**
     * <p>獲取屬性名稱腮鞍,去除前綴</p>
     *
     * @param key   屬性key
     * @param prefix 屬性key前綴
     * @return java.lang.String
     * @author hanqf
     */
    private String fieldName(String key, String prefix) {
        if (StringUtils.hasText(prefix)) {
            return key.replace(prefix + ".", "");
        }
        return key;
    }

    /**
     * <p>將屬性文件值綁定到bean對(duì)象</p>
     *
     * @param bean
     * @param properties
     * @param prefix
     * @author hanqf
     */
    private Object bind(Object bean, Properties[] properties, String prefix) {
        String fieldName = "";//屬性名稱
        String pValue = "";//屬性值
        String[] sp = null; //map屬性分割key和value
        for (Properties pro : properties) {
            Map<String, Map<String, String>> fidleMap = new HashMap<>();
            Map<String, Set<String>> fidleSet = new HashMap<>();
            Map<String, List<String>> fidleList = new HashMap<>();
            //遍歷屬性
            for (Object key : pro.keySet()) {
                pValue = (String) (pro.get(key));
                fieldName = fieldName((String) key, prefix);

                //map
                sp = fieldName.split("\\.");
                if (sp.length == 2) {
                    fieldName = sp[0];
                }

                //list&&set
                if (fieldName.indexOf("[") > 0) {
                    fieldName = fieldName.substring(0, fieldName.indexOf("["));
                }

                //屬性類型
                Object object = getFieldValueByName(fieldName, bean);

                //類型匹配
                if (object instanceof Map) {
                    if (fidleMap.get(fieldName) != null) {
                        object = fidleMap.get(fieldName);
                    } else {
                        object = new HashMap<String, String>();
                    }
                    if (sp.length == 2) {
                        ((Map) object).put(sp[1], pValue);
                        fidleMap.put(fieldName, (Map<String, String>) object);
                    }
                } else if (object instanceof Set) {
                    if (fidleSet.get(fieldName) != null) {
                        object = fidleSet.get(fieldName);
                    } else {
                        object = new HashSet<String>();
                    }
                    ((Set) object).add(pValue);
                    fidleSet.put(fieldName, (Set<String>) object);
                } else if (object instanceof List) {
                    if (fidleList.get(fieldName) != null) {
                        object = fidleList.get(fieldName);
                    } else {
                        object = new ArrayList<String>();
                    }
                    ((List) object).add(pValue);
                    fidleList.put(fieldName, (List<String>) object);
                } else if (object instanceof String) {
                    setFieldValueByName(fieldName, bean, new Class[]{String.class}, new Object[]{pValue});
                } else if (object instanceof Integer) {
                    setFieldValueByName(fieldName, bean, new Class[]{Integer.class}, new Object[]{Integer.valueOf(pValue)});
                } else if (object instanceof Long) {
                    setFieldValueByName(fieldName, bean, new Class[]{Long.class}, new Object[]{Long.valueOf(pValue)});
                } else if (object instanceof Double) {
                    setFieldValueByName(fieldName, bean, new Class[]{Double.class}, new Object[]{Double.valueOf(pValue)});
                } else if (object instanceof Float) {
                    setFieldValueByName(fieldName, bean, new Class[]{Float.class}, new Object[]{Float.valueOf(pValue)});
                }
            }

            //map類型賦值
            if (fidleMap.size() > 0) {
                for (String fname : fidleMap.keySet()) {
                    setFieldValueByName(fname, bean, new Class[]{Map.class}, new Object[]{fidleMap.get(fname)});
                }
            }

            //set類型賦值
            if (fidleSet.size() > 0) {
                for (String fname : fidleSet.keySet()) {
                    setFieldValueByName(fname, bean, new Class[]{Set.class}, new Object[]{fidleSet.get(fname)});
                }
            }

            //list類型賦值
            if (fidleList.size() > 0) {
                for (String fname : fidleList.keySet()) {
                    setFieldValueByName(fname, bean, new Class[]{List.class}, new Object[]{fidleList.get(fname)});
                }
            }

        }

        return bean;
    }


    /**
     * <p>刷新指定屬性類</p>
     * @author hanqf
     * @param beanName bean的注冊(cè)名稱,默認(rèn)類名稱首字母小寫
     */
    @SneakyThrows
    public void refresh(String beanName){
        Class<?> cls = configurableListableBeanFactory.getType(beanName);
        Object bean = configurableListableBeanFactory.getBean(cls);
        Properties[] propertiesArray = null;
        String prefix = "";
        if (cls.getAnnotations() != null && cls.getAnnotations().length > 0) {
            for (Annotation annotation : cls.getAnnotations()) {

                if (annotation instanceof PropertySource) {
                    PropertySource propertySource = (PropertySource) annotation;
                    String[] values = propertySource.value();
                    if (values.length > 0) {
                        propertiesArray = new Properties[values.length];
                        for (int i = 0; i < values.length; i++) {
                            //如果引用的是外部文件莹菱,則重新加載
                            if (values[i].startsWith("file:")) {
                                String path = values[i].replace("file:", "");
                                Properties properties = PropertiesLoaderUtils.loadProperties(new FileSystemResource(path));
                                propertiesArray[i] = properties;
                            }
                        }
                    }
                }

                if (annotation instanceof ConfigurationProperties) {
                    ConfigurationProperties configurationProperties = (ConfigurationProperties) annotation;
                    prefix = configurationProperties.prefix();
                }

            }
        }

        if (propertiesArray != null && propertiesArray.length > 0) {
            //將屬性綁定到對(duì)象
            bind(bean, propertiesArray, prefix);

        }
    }


    /**
     * <p>重新加載屬性文件</p>
     *
     * @author hanqf
     */
    @SneakyThrows
    public void refresh() {
        String[] ary = configurableListableBeanFactory.getBeanNamesForAnnotation(PropertySource.class);
        if (ary != null && ary.length > 0) {
            for (String beanName : ary) {
                //通過Spring的beanName獲取bean的類型
                refresh(beanName);
            }
        }
    }
}

下面通過http請(qǐng)求刷新配置文件

啟動(dòng)服務(wù)器后移国,任意修改屬性文件的值,然后請(qǐng)求/refresh道伟,即可重新加載全部屬性文件迹缀,然后請(qǐng)求/demo查看是否生效,也可以請(qǐng)求/propertiesDemo/refresh蜜徽,指定要刷新的對(duì)象祝懂。

@RestController
public class DemoController {
    @Autowired
    private ExternalPropertiesRefresh externalPropertiesRefresh;

    @Autowired
    private PropertiesDemo propertiesDemo;


    @RequestMapping("/refresh")
    public String refreshpro() {
        externalPropertiesRefresh.refresh();
        return "refresh properties success";
    }

    @RequestMapping("/{beanName}/refresh")
    public String refreshProByBeanName(@PathVariable String beanName) {
        externalPropertiesRefresh.refresh(beanName);
        return "refresh properties success for " + beanName;
    }

    @RequestMapping("/demo")
    public PropertiesDemo demo() {
        return propertiesDemo;
    }

}

PropertiesDemo.java

@Component
@PropertySource(value = "file:demo.properties",encoding = "utf-8")
@ConfigurationProperties(prefix = "demo.data")
@Data
public class PropertiesDemo {
    private Map<String, String> map = new HashMap<>();
    private Map<String, String> map2 = new HashMap<>();
    private Set<String> set = new HashSet<>();
    private Set<String> set2 = new HashSet<>();
    private List<String> list = new ArrayList<>();
    private List<String> list2 = new ArrayList<>();

    private String name;
    private Integer age;
    private Double salary;
}

demo.properties

demo.data.map.client=client
demo.data.map.token=token

demo.data.map2.client=client
demo.data.map2.token=token

demo.data.name=張三
demo.data.age=20
demo.data.salary=12345.67

demo.data.set[0]=beijing
demo.data.set[1]=shanghai
demo.data.set[2]=tianjin

demo.data.set2[0]=guangzhou
demo.data.set2[1]=shenzheng
demo.data.set2[2]=hangzhou


demo.data.list[0]=南極
demo.data.list[1]=北極
demo.data.list[2]=赤道

demo.data.list2[0]=喜馬拉雅山
demo.data.list2[1]=噶麥斯山
demo.data.list2[2]=阿爾卑斯山
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拘鞋,隨后出現(xiàn)的幾起案子砚蓬,更是在濱河造成了極大的恐慌,老刑警劉巖盆色,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灰蛙,死亡現(xiàn)場離奇詭異祟剔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)摩梧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門物延,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仅父,你說我怎么就攤上這事叛薯。” “怎么了笙纤?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵案训,是天一觀的道長。 經(jīng)常有香客問我粪糙,道長强霎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任蓉冈,我火速辦了婚禮城舞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寞酿。我一直安慰自己家夺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布伐弹。 她就那樣靜靜地躺著拉馋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惨好。 梳的紋絲不亂的頭發(fā)上煌茴,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音日川,去河邊找鬼蔓腐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛龄句,可吹牛的內(nèi)容都是我干的回论。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼分歇,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼傀蓉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起职抡,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤葬燎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萨蚕,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蹄胰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岳遥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡裕寨,死狀恐怖浩蓉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宾袜,我是刑警寧澤捻艳,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站庆猫,受9級(jí)特大地震影響认轨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜月培,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一嘁字、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杉畜,春花似錦纪蜒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至灭袁,卻和暖如春猬错,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茸歧。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國打工兔魂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人举娩。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓析校,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铜涉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子智玻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容