Mendmix程式碼解析:百搭的配置檔案讀取工具ResourceUtils

在路上 發表於 2022-06-09

很久很久以前當微服務還沒出現、配置中心還沒出現、yaml配置檔案也還沒流行的時候,我們都習慣在專案裡面寫一個類似ResourceUtils或者PropertiesUtil的工具,無論在靜態方法還是jsp程式碼都屢試不爽。如今Springcloud各種引數化配置、各種profile,Apollo、Nacos各種配置中心以及Properties、Yaml各種配置格式你的配置檔案讀取工具還好麼?接下來我們講解Mendmix專案的ResourceUtils工具類開啟我們Mendmix程式碼解析系列課程的篇章。

介紹

ResourceUtils貫穿了Mendmix專案各個生命週期,在整個專案中被大量使用。目前支援Properties、Yaml配置檔案解析、相容Springcloud的profile配置方式以及無縫相容Apollo、Nacos等各種各樣的配置中心。同時還提供了諸多快速配置檔案讀取的方法,如:getListValue,getMapValue、getBeanValue等。

## 程式碼分析
### 載入過程

通過靜態程式碼塊載入本地配置檔案
static {
       loadLocalConfigs();
}

loadLocalConfigs這個方法首先嚐試獲取spring.profiles.active的值,為了相容通過--spring.profiles.active=prd的方式指定spring.profiles.active的值,Mendmix提供了一個應用啟動類基類BaseApplicationStarter,在這個基類裡面會處理各種執行引數並把設定為系統變數。

為了相容本地執行或打包執行提供了loadPropertiesFromFileloadPropertiesFromJarFile兩個讀取配置的方法,通過這兩個方法會把本地的所有 .properties.yaml檔案找出來(你也攔不住兩種格式的配置檔案混用),並建立<檔案字尾,[檔名稱列表]>的對映關係如下:

{
  ".properties" : [ "/.../application-local.properties", "/.../application.properties" ]
}

接下來呼叫parseSameExtensionFiles方法依次迴圈解析每個字尾名的配置檔案,這個方法做了兩件事情:載入所有不帶profile的配置檔案和找出profile的配置檔案。為了確保profile的配置檔案能覆蓋預設配置,找出的profile的配置檔案放在做好放入解析好的所有配置檔案Properties裡面。這樣整個解析過程就完成了。

處理${}引用配置

直接貼程式碼吧,有點長,類似方法實際上Spring提供的有,僅僅是考慮這只是一個工具類,進來少依賴,所以就自己寫了。

public static String replaceRefValue(Properties properties,String value ) {
        
        if(!value.contains(PLACEHOLDER_PREFIX)){
            return value;
        }
        
        String[] segments = value.split("\\$\\{");
        String seg;
        
        StringBuilder finalValue = new StringBuilder();
        for (int i = 0; i < segments.length; i++) {
            seg = StringUtils.trimToNull(segments[i]);
            if(StringUtils.isBlank(seg))continue;
            
            if(seg.contains(PLACEHOLDER_SUFFIX)){    
                String refKey = seg.substring(0, seg.indexOf(PLACEHOLDER_SUFFIX)).trim();
                //其他非${}的佔位符如:{{host}}
                String withBraceString = null;
                if(seg.contains("{")){
                    withBraceString = seg.substring(seg.indexOf(PLACEHOLDER_SUFFIX)+1);
                }
                
                //如果包含預設值,如:${host:127.0.0.1}
                String defaultValue = null;
                int defaultValSpliterIndex = refKey.indexOf(":");
                if(defaultValSpliterIndex > 0){
                    defaultValue = refKey.substring(defaultValSpliterIndex + 1);
                    refKey = refKey.substring(0,defaultValSpliterIndex);
                }
                
                String refValue = System.getProperty(refKey);
                if(StringUtils.isBlank(refValue))refValue = System.getenv(refKey);
                if(StringUtils.isBlank(refValue))refValue = properties.getProperty(refKey);
                if(StringUtils.isBlank(refValue)){
                    refValue = defaultValue;
                }
                
                if(StringUtils.isBlank(refValue)){
                    finalValue.append(PLACEHOLDER_PREFIX + refKey + PLACEHOLDER_SUFFIX);
                }else{
                    finalValue.append(refValue);
                }
                
                if(withBraceString != null){
                    finalValue.append(withBraceString);
                }else{
                    String[] segments2 = seg.split("\\}");
                    if(segments2.length == 2){
                        finalValue.append(segments2[1]);
                    }
                }
            }else{
                finalValue.append(seg);
            }
        }

整合配置中心

考慮到各種各樣的配置中心,所以我們不能與具體配置中心產品繫結。所以Mendmix從Spring載入的生命週期下手。在Environment物件載入完成後對所有配置進行一次合併,程式碼如下:

private static void mergeEnvironmentProperties(){
        MutablePropertySources propertySources = ((ConfigurableEnvironment)environment).getPropertySources();
        
        int count;
        for (PropertySource<?> source : propertySources) {
            if(source.getName().startsWith("servlet") || source.getName().startsWith("system")){
                continue;
            }
            if(source.getName().contains("applicationConfig: [classpath")) {
                continue;
            }
            count = 0;
            if (source instanceof EnumerablePropertySource) {
                for (String name : ((EnumerablePropertySource<?>) source) .getPropertyNames()) {
                    Object value = source.getProperty(name);
                    if(value != null){
                        ResourceUtils.add(name, value.toString());
                        count++;
                    }
                }
            }
            System.out.println(">>merge PropertySource:" + source.getName() + ",nums:" + count);
        }
    }

該類在com.mendmix.spring.helper.EnvironmentHelper,這裡要注意兩點:

  1. 確保在應用bean初始化之前完成合並
  2. 要跳過本地配置合併,否則可能出現遠端配置又本本地配置覆蓋的情況。

## 用法舉例
假如有一份這樣的配置檔案

whitelist.ips=10.1.1.10;10.1.1.100
#aliyun OSS
mendmix.cos.adapter.type=aliyun
mendmix.cos.adapter.accessKey=5tHzzxhTs45tbUrKgTHYxxxx
mendmix.cos.adapter.secretKey=aIDWMP2pbvFjML7tYAzfPXXXXXXX
mendmix.cos.adapter.regionName=cn-guangzhou
#feign代理
mendmix.loadbalancer.customize.mapping[mendmix-user-svc]=http://127.0.0.1:8081
mendmix.loadbalancer.customize.mapping[mendmix-order-svc]=http://127.0.0.1:8082

部分用法

//查詢指定字首的配置
Properties properties = ResourceUtils.getAllProperties("mendmix.cos.adapter");
//查詢指定字首並返回物件
CosConfig cosConfig = ResourceUtils.getBean("mendmix.cos.adapter", CosConfig.class);
//KV格式的配置
Map<String, String> mappingValues = ResourceUtils.getMappingValues("mendmix.loadbalancer.customize.mapping");
//返回列表
List<String>  whitelistIps = ResourceUtils.getList(" whitelist.ips");
//    多個配置相容
ResourceUtils.getAnyProperty("key1","key2");

---

關於Mendmix

Mendmix基於Apache 2.0開源協議,100%開源。定位是一站式分散式開發架構開源解決方案及雲原生架構技術底座。Mendmix提供了資料庫、快取、訊息中介軟體、分散式定時任務、安全框架、閘道器以及主流產商雲服務快速整合能力。基於Mendmix可以不用關注技術細節快速搭建高併發高可用基於微服務的分散式架構。

專案地址:github,gitee