天天看點

利用META-INF/spring.factories實作springboot自定義啟動配置管理介紹

介紹

       微服務化是的我們需要管理的服務增多。同時每一個服務有各自的配置,對于同一個配置來說,開發、應用不同時刻所用的檔案不同,這樣會導緻配置的維護更加的複雜和困難。spring cloud config可以實作統一的配置管理,但是在很多情況下不是很友善。我們需要根據自己的需要來定義的自己的啟動配置。參照spring-cloud的方式,在這裡實作一種自定義的服務啟動配置管理。差別是加入自定義的配置解析方式。

利用META-INF/spring.factories實作springboot自定義啟動配置管理介紹

記錄好公共的配置資訊(consul、mq等),以及各個服務私有配置定資訊。公共的配置資訊是不變的, 私有配置定信可以在各個微服務安裝之前就定義好。安裝的時候就告訴配置管理服務,配置管理服務使用界面展示這些資訊,是使用者可配置。在配置成功之後重新開機這些服務即可。

原理

使用PropertiesPropertySource類,在啟動時更改環境變量,并不修改application.*(properties/yml)中的配置。

啟動順序:bootstrap.* > propertiesPropertySource > application.*

優先級:propertiesPropertySource > bootstrap.* > application.*

即同樣的配置項,在propertiesPropertySource和application.properties都配置過的,會優先使用propertiesPropertySource中的配置。

實作

        采用spring.factories配置PropertySourceLocator的配置實作類。在程式啟動的時候解析和擷取自定義的配置資訊。首先startupConfiig.xml中定義了項目依賴的配置項,例如資料配置資訊、consul、redis等。而ConfigLocalPropertySourceLocator 中定義了解析startupConfiig.xml内容根據内容的定義去config.properties中擷取相應的配置資訊(上下文路徑、端口、資料配置資訊、consul、redis等)并将資訊加載到項目中。config.properties的配置是預先設定好的,由統一配置管理按照一定的規則生成。當這裡我們也不一定要解析config.properties的内容擷取資訊,也可以通過遠端調用的方式來擷取配置資訊。總之就看實際需要實作即可。

startupConfiig.xml檔案内容:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <!--實際應用中使用的配置   config.properties中的配置項是 [instances.service].[instance.service].[instance.order]  擷取-->
    <instances service="springDemoService">
        <!-- 服務執行個體相關設定 -->
        <instance type="service" name="service" order="1"/>
        <!-- 資料庫相關設定   使用資料庫配置為 db-->
        <instance type="database" name="db" order="1" dbType=""/>
        <!-- mq相關設定-->
        <instance type="rabbitmq" name="rabbitmq" order="1"/>
        <!-- consul相關設定-->
        <instance type="consul" name="consul" order="1"/>
        <!-- 緩存配置-->
        <instance type="cache" name="cache" order="1"/>
    </instances>
    <!--實際使用中的一些參數-->
    <parameters service="springDemoService">
        <!--  envKey:加載到環境中的資料的key值    key:config配置檔案中的字尾    defaultValues:預設值,以“,”隔開,  order:取值的順序 -->
        <parameter envKey="testKey1"  key="testKey1" defaultValues="testValue1" order="1"/>
        <parameter envKey="testKey1"  key="testKey2" defaultValues="testValue2" order="1"/>
        <parameter envKey="testKey1"  key="testKey3" defaultValues="testValue3" order="1"/>
    </parameters>
</config>
           

config.properties内容:

#使用的配置設定
[email protected]=springDemoService-cache
[email protected]=springDemoService-db
[email protected]=springDemoService-service
[email protected]=springDemoService-consul
#服務配置
[email protected]=/spring-demo
[email protected]=DEBUG
[email protected]=8109ea47-7386-4704-8dce-5a3443cc777f
springDemoService-service.webPort=8081
[email protected]=10.16.65.13
#資料庫配置
springDemoService-db.1.instance=test
[email protected]=test_db
[email protected]=1.1.1
[email protected]=postgresql
[email protected]=00753ba3-857c-490a-be01-376ac36a82cf
[email protected]=cG9zdGdyZXM=
[email protected]=test
springDemoService-db.1.port=5432
[email protected]=localhost
#注冊中心配置
[email protected]=/VIID
[email protected]=DEBUG
[email protected]=8109ea47-7386-4704-8dce-5a3443cc777f
springDemoService-consul.1.webPort=8068
[email protected]=10.19.155.2
#緩存配置
[email protected]=1.2.0
[email protected]=11543d67-7824-4c57-b1b7-04d954919b89
[email protected]=redislinux64
[email protected]=MTIzNDU2
[email protected]=
springDemoSe[email protected]=127.0.0.1
springDemoService-cache.1.port=6379
           

spring.factories配置:

# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
zhong.test.springbootdemo.usultestdemo.configration.propertiespropertysource.ConfigLocalConfiguration
           

配置加載:

@Configuration
public class ConfigLocalConfiguration {
    public ConfigLocalConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean({ConfigLocalPropertySourceLocator.class})
    public ConfigLocalPropertySourceLocator configLocalPropertySourceLocator() {
        return new ConfigLocalPropertySourceLocator();
    }
}
           

實作了程式中依賴的各種配置的擷取方式。

配置是否生效判斷

ConfigLocalPropertySourceLocator的locate(Environment environment)方法中我們需要實作自定義的配置邏輯,

@SneakyThrows
    @Override
    public PropertySource<?> locate(Environment environment) {
       //記錄最終加載到運作環境中的資料
        Properties localConfig = new Properties();
        if (init()) {
            propertyConvertor(localConfig);
        } else {
            LOGGER.warn("ConfigLocalPropertySourceLocator init failed!");
        }

        decryConfig(localConfig);
        //記錄最終加載到運作環境中的資料

        return new PropertiesPropertySource("localConfig", localConfig);
    }
           

其中init()方法是是擷取是否存在startupCoonfig.xml和config.properties檔案存在,如果存在startupCoonfig.xml則繼續解析congig.properties中的内容

/**
     * 初始化startupConfig.xml的配置資訊
     * 将config.properties的配置資訊加載到變量中。
     * @return
     */
    boolean init() {
        File configDir = null;
        if (null == this.startupConfigFile) {
            try {
                //擷取 startupConfig.xml 的位置
                setStartupConfigXml();
                //如果配置存在就加載config.properties. 相容本地開發環境,開發時不需要使用 startupConfig.xml 和 config.properties
                if(startupConfigFile != null){
                    //擷取 config.properties 的位置
                    configDir = getConfigPropertyPath();
                    //加載config.properties中的資料
                    loadConfig(configDir);
                }
            } catch (FileNotFoundException var3) {
                LOGGER.error("file don't exists", var3);
                System.exit(-1);
            } catch (Exception var4) {
                LOGGER.error("startup-config init failed", var4);
            }
        }
        return null != this.configroperties;
    }

/**
     * 加載指定路徑的 config.properties
     * @param configDir 檔案
     */
    private void loadConfig(File configDir) {
        File configFile = null;
        configFile = new File(configDir.getAbsolutePath() + "/" + "config.properties");
        if (!configFile.exists()) {
            try {
                configFile = ResourceUtils.getFile("classpath:config.properties");
            } catch (IOException e) {
                LOGGER.error("config.properties don't exists", e);
                System.exit(-1);
            }
        }

        try {
            this.propertiesLoad(configFile, "config.properties", this.configroperties);
        } catch (Exception e) {
            LOGGER.error("load config.properties failed, target key don't exists", e);
            System.exit(-1);
        }

    }

/**
     * 擷取 startupConfig.xml 的位置
     * @throws Exception 異常
     */
    private void setStartupConfigXml() throws Exception {
        ClassPathResource startupConfigXmlRes = new ClassPathResource("startupConfig.xml");

        try {
            this.startupConfigFile = startupConfigXmlRes.getFile();
        } catch (IOException var3) {
            LOGGER.error("get startupConfig.xml error. program will exit", var3);
            throw new Exception("startupConfig.xml does not exists, configuration of application.properties be used");
        }

        if (!this.startupConfigFile.exists()) {
            LOGGER.error("startupConfig.xml does not exist in classpath, configuration of application.properties be used");
            throw new Exception("startupConfig.xml does not exists");
        } else {
            LOGGER.info(String.format("StartupConfig.xml File exists, path=[%s]: ", this.startupConfigFile));
        }
    }

    /**
     * 指定 config.properties檔案的位置 , 項目的安裝位置的  /conf/config.properties   開發時可為空
     * @return
     */
    private File getConfigPropertyPath(){
        String userDir = System.getProperty("user.dir");
        File serviceDir = new File(userDir);
        LOGGER.info(String.format("userDir=[%s]",serviceDir.getAbsolutePath()));
        if (serviceDir.exists() && serviceDir.isDirectory()) {
            File componentDir;
            try {
                componentDir = this.getComponentDir(serviceDir);
            } catch (NullPointerException var6) {
                componentDir = new File(System.getProperty("user.dir"));
            }
            //生産環境下面的路徑是需要有的。應該預設建立或者安裝的時候建立
            if (componentDir.exists() && componentDir.isDirectory()) {
                String componentConfigDir = StringUtils.replace(componentDir.getAbsolutePath() + "/conf", "//", "/");
                LOGGER.info(String.format("componentConfigDir=[]", componentConfigDir));
                File componentConfigFile = new File(componentConfigDir);
                if (componentConfigFile.exists() && componentConfigFile.isDirectory()) {
                    return componentConfigFile;
                }
                LOGGER.error("componentInstallPath/conf/ don't exists");
            } else {
                LOGGER.error("componentInstallPath/conf/ don't exists");
            }
        } else {
            LOGGER.error("user.dir don't exists");
        }
        //走到這裡說明是開發環境
        return new File(Thread.currentThread().getContextClassLoader().getResource(".").getPath());
    }
 /**
     * 擷取項目的安裝路徑
     * @param serviceDir 服務路徑
     * @return
     * @throws NullPointerException
     */
    private File getInstallDir(File serviceDir) throws NullPointerException {
        File componentDir = new File(System.getProperty("user.dir"));

        try {
            componentDir = serviceDir.getParentFile().getParentFile().getParentFile();
        } catch (Exception var4) {
            LOGGER.error("get component path error, but the program go on", var4);
        }

        LOGGER.info(String.format("componentDir=[]", componentDir.getAbsolutePath()));
        return componentDir;
    }
           

propertyConverter負責解析startupCoonfig.xml中的内容,包括項目使用配置(上下文路徑、端口、資料配置資訊、consul、redis等),以及自定義的變量内容。

/**
     * 解析 startupConfig.xml中的項
     * @param localConfig
     */
    void propertyConverter(Properties localConfig) {
        try {
            SAXReader sax = new SAXReader();
            Document dom = sax.read(this.startupConfigFile);
            Element configRoot = dom.getRootElement();
            Iterator iterator = configRoot.elements().iterator();

            //解析所有節點
            while (iterator.hasNext()) {
                Element element = (Element) iterator.next();
                String eleName = element.getName();
                //各種應用執行個體配置
                if (eleName.equals("instances")) {
                    //依賴配置解析實作
                    InstanceHandle.handle(element, localConfig, this.configroperties, this.myServiceInstance);
                }
                //動态參數配置
                if(eleName.equals("parameters")){
                    //配置參數解析實作
                    ParameterHandle.handle(element, localConfig, this.configroperties);
                }
                //其他需要可以自定義
            }

        } catch (DocumentException e) {
            LOGGER.error("Fail to parse file config.properties or startupConfig.xml exception", e);
        }
    }
           

依賴的服務解析

InstanceHandle 是對項目  上下文路徑、端口、資料配置資訊、consul、redis 的資訊的解析。

/**
 * 服務依賴配置資訊解析
 * @date 2020/12/16 - 16:23
 */
public class InstanceHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHandle.class);

    public static void handle(Element insNode, Properties localConfig, Properties configs, ServiceInstance myServiceInstance) {
        String servicePrefix = insNode.attributeValue("service");

        if (StringUtils.isNotEmpty(servicePrefix)) {
            Iterator iterator = insNode.elements().iterator();
            while (iterator.hasNext()) {
                Element node = (Element) iterator.next();
                //擷取 每個 <instance>節點的資料
                String insType = node.attributeValue("type");
                String insName = node.attributeValue("name");
                String keyOrder = node.attributeValue("order");
                //配置項
                String instancePrefix = servicePrefix + "-" + insName;
                //通過配置項擷取配置的字首
                String instanceValue = StringUtils.isNotBlank(insName) ? CommonTool.selectInstance(instancePrefix + "[email protected]", keyOrder, configs) : null;
                //通過配置項解析每一個種類的配置資料
                switch (insType) {
                    case "consul":
                        //consul位址配置
                        TypeConsulHandle.handle(instanceValue, myServiceInstance, keyOrder, localConfig, configs);
                        break;
                    case "rabbitmq":
                        //rabbitmq位址配置
                        TypeRabbitmqHandle.handle(instanceValue, keyOrder, localConfig, configs);
                        break;
                    case "cache":
                        //redis配置
                        if (null != instanceValue) {
                            TypeCacheHandle.handle(instanceValue, configs, keyOrder, localConfig);
                        } else {
                            LOGGER.warn(String.format("Lack of instance list name of type=[%s] in config.properties!", insType));
                        }
                        break;
                    case "database":
                        //資料庫配置
                        String plugin = node.attributeValue("plugin");
                        String pgdialect = node.attributeValue("pgdialect");
                        String oracledialect = node.attributeValue("oracledialect");
                        if (null != instanceValue) {
                            TypeDataBaseHandle.handle(localConfig, configs, plugin, instanceValue, keyOrder, pgdialect, oracledialect);
                        } else {
                            LOGGER.warn(String.format("Lack of instance list name of type=[%s] in config.properties!", insType));
                        }
                        break;
                    case "service":
                        //服務配置
                        System.setProperty("spring.session.redis.namespace", insName);
                        myServiceInstance.setServiceId(instancePrefix);
                        if (null != instanceValue) {
                            TypeServiceHandle.handle(instanceValue, myServiceInstance, localConfig, configs);
                        } else {
                            LOGGER.warn(String.format("Lack of instance list name of type=[%s] in config.properties! ", insType));
                        }
                        break;
                    default:
                        LOGGER.warn(String.format("can't support instance type,type=[%s]: ", insType));
                }
            }

        } else {
            LOGGER.error(String.format("cannot loading the service prefix from the node [instances]"));
            System.exit(-1);
        }
    }
}
           

下面展示項目的配置(端口、上下文路徑,端口等)和資料庫配置

服務配置資訊解析:

public class TypeServiceHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(TypeServiceHandle.class);

    public TypeServiceHandle() {
    }

    public static void handle(String instanceValue, ServiceInstance myServiceInstance, Properties localConfig, Properties bicConfigs) {
        String bicValue = null;
        myServiceInstance.setInstanceValue(instanceValue);
        bicValue = bicConfigs.getProperty(instanceValue + ".webPort");
        CommonTool.setLocalConfigAndMyServiceInstance("server.port", bicValue, myServiceInstance::setWebPort, localConfig, instanceValue + ".webPort");
        bicValue = bicConfigs.getProperty(instanceValue + "[email protected]");
        CommonTool.setLocalConfigAndMyServiceInstance("server.context-path", bicValue, myServiceInstance::setContext, localConfig, instanceValue + "[email protected]");
        CommonTool.setLocalConfigAndMyServiceInstance("server.servlet.context-path", bicValue, myServiceInstance::setContext, localConfig, instanceValue + "[email protected]");
        bicValue = bicConfigs.getProperty(instanceValue + "[email protected]");
        CommonTool.setLocalConfigAndMyServiceInstance("server.ip", bicValue, myServiceInstance::setAddress, localConfig, instanceValue + "[email protected]");
        myServiceInstance.setIndexCode(bicConfigs.getProperty(instanceValue + "[email protected]"));
    }
}
           

資料庫資訊解析

相容了oracle和pg的配置,也可以加入mysql等

public class TypeDataBaseHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(TypeDataBaseHandle.class);

    public static void handle(Properties localConfig, Properties bicConfigs, String plugin, String dbInstance, String keyOrder, String pgDialect, String oracleDialect) {
        String keyPrefix = dbInstance + "." + keyOrder + ".";

        String dbtype = bicConfigs.getProperty(keyPrefix + "@type");

        String dbIp = CommonTool.getFirstIp(bicConfigs.getProperty(keyPrefix + "@ip"));
        String dbPort = bicConfigs.getProperty(keyPrefix + "port");
        String dbUsername = bicConfigs.getProperty(keyPrefix + "@dbusername");
        String dbPassword = bicConfigs.getProperty(keyPrefix + "@dbpassword");
        String dbName = bicConfigs.getProperty(keyPrefix + "@dbname");

        //oracle使用
        String dbInstanceName = bicConfigs.getProperty(keyPrefix + "instance");
        //測試是否可用
        CommonTool.portIsAvailableOrExit(dbIp, dbPort, "db");
        if (StringUtils.isNoneEmpty(new CharSequence[]{dbtype, dbPort, dbName, dbUsername, dbPassword})) {
            localConfig.setProperty("spring.datasource.username", dbUsername);
            localConfig.setProperty("spring.datasource.password", CommonTool.decode(dbPassword));
            byte dbPlugins;
            if (StringUtils.contains(dbtype, "postgresql")) {
                //使用pg 資料庫
                localConfig.setProperty("spring.datasource.name", "postgresql");
                if (StringUtils.isNotBlank(plugin)) {
                    if (plugin.hashCode() == 102353 && plugin.equals("gis")) {
                        localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.spatial.dialect.postgis.PostgisDialect");
                    } else {
                        localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.dialect.PostgreSQL9Dialect");
                    }
                } else {
                    localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.dialect.PostgreSQL9Dialect");
                }

                if (StringUtils.isNotBlank(pgDialect)) {
                    localConfig.setProperty("spring.jpa.properties.hibernate.dialect", pgDialect);
                }

                localConfig.setProperty("spring.datasource.url", "jdbc:postgresql://" + dbIp + ":" + dbPort + "/" + dbName);
                localConfig.setProperty("spring.datasource.driver-class-name", "org.postgresql.Driver");
                localConfig.setProperty("spring.datasource.validationQuery", "SELECT 1");
            } else if (StringUtils.contains(dbtype, "oracle")) {
                //使用oracle 方式連接配接資料庫
                localConfig.setProperty("spring.datasource.name", "oracle");
                if (StringUtils.isNotBlank(plugin)) {
                         if (plugin.equals("gis")) {
                             localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.spatial.dialect.oracle.OracleSpatial11gDialect");
                         } else {
                             localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect");
                         }
                } else {
                    localConfig.setProperty("spring.jpa.properties.hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect");
                }

                if (StringUtils.isNotBlank(oracleDialect)) {
                    localConfig.setProperty("spring.jpa.properties.hibernate.dialect", oracleDialect);
                }

                localConfig.setProperty("spring.datasource.url", "jdbc:oracle:thin:@" + dbIp + ":" + dbPort + ":" + dbInstanceName);
                localConfig.setProperty("spring.datasource.driver-class-name", "oracle.jdbc.OracleDriver");
                localConfig.setProperty("spring.datasource.validationQuery", "select 'x' FROM DUAL");
            } else {
                LOGGER.warn("Error in database @type in config.properties");
            }
        } else {
            LOGGER.warn("Lack of some database configurations in config.properties");
        }
        LOGGER.debug(String.format("Database configuration: dbUrl=[]  : ", localConfig.getProperty("spring.datasource.url", "")));
    }
}
           

consul資訊配置解析

public class TypeConsulHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(TypeConsulHandle.class);

    public static void handle(String instanceValue, ServiceInstance myServiceInstance, String keyOrder, Properties localConfig, Properties bicConfigs) {
        LOGGER.debug(String.format("Loading consul configurations from config.properties, instance= [%s]", instanceValue));
        localConfig.setProperty("spring.cloud.consul.host", bicConfigs.getProperty(instanceValue + "." + keyOrder + "[email protected]"));
        localConfig.setProperty("spring.cloud.consul.port", bicConfigs.getProperty(instanceValue + "." + keyOrder + ".webPort"));

        localConfig.setProperty("spring.cloud.consul.discovery.preferIpAddress", "true");
        if (StringUtils.isNotBlank(myServiceInstance.getAddress())) {
            localConfig.setProperty("spring.cloud.consul.discovery.ip-address", CommonTool.getFirstIp(myServiceInstance.getAddress()));
            if (StringUtils.isNotBlank(myServiceInstance.getContext())) {
                localConfig.setProperty("spring.cloud.consul.discovery.health-check-path", myServiceInstance.getContext() + "/health");
            } else {
                localConfig.setProperty("spring.cloud.consul.discovery.health-check-path", "/health");
            }
        } else {
            LOGGER.warn("Cannot set consul.discovery.ip-address, please set your service instance in startupConfig.xml");
        }

        localConfig.setProperty("spring.cloud.consul.discovery.health-check-interval", "20s");
        Optional<String> instanceIndexCode = Optional.ofNullable(myServiceInstance.getIndexCode());
        instanceIndexCode.ifPresent((s) -> {
            localConfig.setProperty("spring.cloud.consul.discovery.instance-id", myServiceInstance.getServiceId() + "-" + s);
        });
        CommonTool.portIsAvailableOrExit(localConfig.getProperty("spring.cloud.consul.host", ""), localConfig.getProperty("spring.cloud.consul.port", ""), "consul");
    }
}
           

redis資訊配置解析

public class TypeCacheHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(TypeCacheHandle.class);

    public TypeCacheHandle() {
    }

    /**
     * 擷取緩存配置資訊
     *
     * @param instanceValue 緩存字首
     * @param bicConfigs    所有配置資訊
     * @param keyOrder      使用的配置順序
     * @param localConfig   本地配置項
     */
    public static void handle(String instanceValue, Properties bicConfigs, String keyOrder, Properties localConfig) {
        String bicValue = null;
        String keyPrefix = instanceValue + "." + keyOrder + ".";

        String ipKey = keyPrefix + "@ip";
        bicValue = bicConfigs.getProperty(ipKey);
        CommonTool.setLocalConfig("spring.redis.host", CommonTool.getFirstIp(bicValue), localConfig, ipKey);

        String portKey = keyPrefix + "port";
        bicValue = bicConfigs.getProperty(portKey);
        CommonTool.setLocalConfig("spring.redis.port", bicValue, localConfig, portKey);

        String pwdKey = keyPrefix + "@password";
        bicValue = bicConfigs.getProperty(pwdKey);
        CommonTool.setLocalConfig("spring.redis.password", CommonTool.decode(bicValue), localConfig, pwdKey);

        CommonTool.setLocalConfig("management.health.redis.enabled", "false", localConfig, "management.health.redis.enabled");
        CommonTool.portIsAvailableOrExit(localConfig.getProperty("spring.redis.host", ""), localConfig.getProperty("spring.redis.port", ""), "cache");
    }
}
           

       在上面的各種配置中都是解析的是config中有的配置資訊。有時候我們完全不需要配置這些資訊。我們隻需要有一個配置中心提供這些資訊(資料庫、redis、consul、服務上下文和端口、mq)的查詢接口,同時config.properties中記錄了配置中心的ip和端口資訊。而我們需要實作的是根據startupConfi.xml的配置,從config.properties取出服務ip和端口然後調用接口查詢,最後加載到環境中。

依賴配置解析

參數的解析相對簡單,隻要對應解析出key、預設值、取值排序然後組裝。最後放到環境中也可以。這種方式也可以通過遠端從配置中心擷取的方式。也可以提供界面做配置修改,然後儲存到config.properties中,重新開機服務就可以加載到資訊。

/**
 * 參數解析
 * @date 2020/12/18 - 17:20
 */
public class ParameterHandle {
    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHandle.class);

    /**
     * 解析各種參數配置
     * @param element 參數配置節點
     * @param localConfig 加載到環境中的配置項
     * @param configs config.properties的值
     */
    public static void handle(Element element, Properties localConfig, Properties configs){
        Iterator<Element> iterator = element.elements().iterator();
        String paramPrefix = element.attributeValue("service");
        if(StringUtils.isNotEmpty(paramPrefix)) {
            while (iterator.hasNext()) {
                Element subElement = iterator.next();
                String keyName = subElement.attributeValue("key");
                String defaultValues = subElement.attributeValue("defaultValues");
                String envKey = subElement.attributeValue("envKey");
                int order = StringUtils.isEmpty(subElement.attributeValue("order")) ? 1 : Integer.valueOf(subElement.attributeValue("order"));
                String[] values = defaultValues.split(",");
                String defaultValue = values.length >= order ? values[order - 1] : null;
                setValue(paramPrefix+ "."+ keyName, configs, localConfig, order, defaultValue, envKey);
            }
        }else {
            LOGGER.warn("Fail to get the value of [service] from node [parameters]");
        }
    }

    private static void setValue(String instanceName, Properties localConfig, Properties configs, int order, String defaultValue, String envKey){
        String instanceNameValues = configs.getProperty(instanceName + "." + order);
        if(StringUtils.isNotEmpty(instanceNameValues)) {
            String[] values = instanceNameValues.split(",");
            String value = values.length > order ? values[order - 1] : null;
            if (StringUtils.isNotEmpty(value)) {
                localConfig.setProperty(envKey, value);
            } else if (StringUtils.isNotEmpty(defaultValue)) {
                localConfig.setProperty(envKey, defaultValue);
            } else {
                LOGGER.warn(String.format("Value of [%s] order [%s] is null. setting of config.properties is [%s]. default value is [%s]", envKey, order, instanceNameValues, defaultValue));
            }
        } else if(StringUtils.isNotEmpty(defaultValue)){
            localConfig.setProperty(envKey, defaultValue);
        } else {
            LOGGER.warn(String.format("Value of [%s] order [%s] is null. setting of config.properties is [%s]. default value is [%s]", envKey, order, instanceNameValues, defaultValue));
        }
    }
}
           

https://blog.csdn.net/f641385712/article/details/84401629

https://www.cnblogs.com/lizo/p/7683300.html

https://blog.csdn.net/qq_31086797/article/details/107500079

https://www.cnblogs.com/zhangjianbin/p/6322476.html

https://cloud.tencent.com/developer/article/1497673

https://www.imooc.com/article/299972