Spring自定義加載配置文件(分層次加載)

前言:

    Spring會默認加載application.properties文件,我們一般可以將配置寫在此處。這基本可以滿足我們的常用demo項目使用。

    但是在實際項目開發中,我們會將配置文件外置,這樣在我們需要修改配置的時候就不用將項目重新打包部署了。

    下面我們來看一下實際項目開發的需求。

 

針對配置分層次加載的需求:

    舉給例子:

    1.我們希望項目啓動後會加載內部配置文件(統一命名爲env.properties)

    2.如果有外置配置文件的話(路徑設置爲/envconfig/${app.name}/env.properties),則加載外置配置文件,並覆蓋內部配置文件的相同key的項

    3.如果在項目啓動時候指定了命令行參數,則該參數級別最高,可以覆蓋外置配置文件相同key的項

 

    以上這個需求,我們用目前Spring的加載配置的方式就有點難以完成了。所以這時候我們需要自定義加載方式。

 

環境準備:

    筆者新建了一個SpringBoot項目,maven基本配置如下:

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
    </dependencies>

自定義配置加載器:

    1.配置加載器processor

/**
 * 客戶端自定義加載配置
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class CustomerConfigLoadProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        // 我們將主要邏輯都放在ConfigLoader去做
        environment.getPropertySources().addFirst(new ConfigLoader().getPropertySource());
    }
}

    2.在/resources/META-INF/下創建spring.factories文件,並添加

org.springframework.boot.env.EnvironmentPostProcessor=com.xw.study.configload.processor.CustomerConfigLoadProcessor

    3.實現配置加載邏輯

        以上spring environment框架搭建好之後,在項目啓動時候就會去加載ConfigLoader對應的Properties信息到當前運行環境中。

        下面就來看下加載邏輯:

/**
 * 配置加載器
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class ConfigLoader {

    private static Properties prop = new Properties();
    public static final String DEFAULT_CONFIG_FILE_NAME = "env.properties";
    public static final String SLASH = File.separator;

    public ConfigLoader() {
        loadProperties();
    }

    /**
     * 加載配置文件分爲三個層次
     * 1.加載項目內置classpath:env.properties
     * 2.加載外部配置文件env.properties(會給定一個默認路徑)
     * 3.加載JVM命令行參數
     */
    private void loadProperties() {
        loadLocalProperties();
        loadExtProperties();
        loadSystemEnvProperties();
    }

    /**
     * 加載JVM命令行參數、Environment參數
     */
    private void loadSystemEnvProperties() {
        prop.putAll(System.getenv());
        prop.putAll(System.getProperties());
    }

    /**
     * 加載外部配置文件env.properties(會給定一個默認路徑)
     * 筆者所在公司,會根據不同的項目名,統一路徑設置爲
     * /envconfig/{app.name}/env.properties
     */
    private void loadExtProperties() {
        // 獲取全路徑
        // 所以需要首先在內部env.properties中配置上app.name
        if (prop.containsKey("app.name")) {
            String appName = prop.getProperty("app.name");
            String path = SLASH + "envconfig" + SLASH + appName + SLASH + DEFAULT_CONFIG_FILE_NAME;

            Properties properties = ConfigUtil.loadProperties(path);
            if (null != properties) {
                prop.putAll(properties);
            }
        }
    }


    /**
     * 對外提供的方法,獲取配置信息
     * @param key key
     * @return 配置值
     */
    public static String getValue(String key) {
        return prop.getProperty(key);
    }


    /**
     * 加載項目內置classpath:env.properties
     */
    private void loadLocalProperties() {
        Properties properties = ConfigUtil.loadProperties(ConfigUtil.CLASSPATH_FILE_FLAG + DEFAULT_CONFIG_FILE_NAME);
        if (null != properties) {
            prop.putAll(properties);
        }
    }

    // 提供給environment.getPropertySources()的加載方法
    public PropertiesPropertySource getPropertySource() {
        return new PropertiesPropertySource("configLoader", prop);
    }
}

    工具類:ConfigUtil

/**
 * 工具類
 * 直接從Sentinel項目拷貝過來的
 *
 * @author lucky
 * @create 2020/3/7
 * @since 1.0.0
 */
public class ConfigUtil {
    public static final String CLASSPATH_FILE_FLAG = "classpath:";

    /**
     * <p>Load the properties from provided file.</p>
     * <p>Currently it supports reading from classpath file or local file.</p>
     *
     * @param fileName valid file path
     * @return the retrieved properties from the file; null if the file not exist
     */
    public static Properties loadProperties(String fileName) {
        if (StringUtils.isNotBlank(fileName)) {
            if (absolutePathStart(fileName)) {
                return loadPropertiesFromAbsoluteFile(fileName);
            } else if (fileName.startsWith(CLASSPATH_FILE_FLAG)) {
                return loadPropertiesFromClasspathFile(fileName);
            } else {
                return loadPropertiesFromRelativeFile(fileName);
            }
        } else {
            return null;
        }
    }

    private static Properties loadPropertiesFromAbsoluteFile(String fileName) {
        Properties properties = null;
        try {

            File file = new File(fileName);
            if (!file.exists()) {
                return null;
            }

            try (BufferedReader bufferedReader =
                         new BufferedReader(new InputStreamReader(new FileInputStream(file), getCharset()))) {
                properties = new Properties();
                properties.load(bufferedReader);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return properties;
    }

    private static boolean absolutePathStart(String path) {
        File[] files = File.listRoots();
        for (File file : files) {
            if (path.startsWith(file.getPath())) {
                return true;
            }
        }
        return false;
    }


    private static Properties loadPropertiesFromClasspathFile(String fileName) {
        fileName = fileName.substring(CLASSPATH_FILE_FLAG.length()).trim();

        List<URL> list = new ArrayList<>();
        try {
            Enumeration<URL> urls = getClassLoader().getResources(fileName);
            list = new ArrayList<>();
            while (urls.hasMoreElements()) {
                list.add(urls.nextElement());
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

        if (list.isEmpty()) {
            return null;
        }

        Properties properties = new Properties();
        for (URL url : list) {
            try (BufferedReader bufferedReader =
                         new BufferedReader(new InputStreamReader(url.openStream(), getCharset()))) {
                Properties p = new Properties();
                p.load(bufferedReader);
                properties.putAll(p);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        return properties;
    }

    private static Properties loadPropertiesFromRelativeFile(String fileName) {
        return loadPropertiesFromAbsoluteFile(fileName);
    }

    private static ClassLoader getClassLoader() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null) {
            classLoader = ConfigUtil.class.getClassLoader();
        }
        return classLoader;
    }

    private static Charset getCharset() {
        // avoid static loop dependencies: SentinelConfig -> SentinelConfigLoader -> ConfigUtil -> SentinelConfig
        // so not use SentinelConfig.charset()
        return Charset.forName(System.getProperty("csp.sentinel.charset", StandardCharsets.UTF_8.name()));
    }

    public static String addSeparator(String dir) {
        if (!dir.endsWith(File.separator)) {
            dir += File.separator;
        }
        return dir;
    }

    public ConfigUtil() {
    }
}

    代碼不算複雜,筆者不再詳述。

    根據以上的加載順序,就可以實現 命令行 > 外部配置文件 > 內部配置文件的需求。

 

    4.測試

        這個比較簡單了,用戶可自行測試

        1)只有內部配置文件

            在/resources下創建env.properties文件

        2)內部配置文件、外部配置文件均存在

            滿足1)的同時(注意有一個必備項爲app.name,筆者自定義爲configload),在本地磁盤創建/envconfig/configload/env.properties文件

        3)添加命令行參數

            在滿足2)的同時,在啓動行添加參數(-D的方式)

 

        筆者測試代碼:

@SpringBootTest(classes = ConfigloadApplication.class)
@RunWith(SpringRunner.class)
public class ConfigloadApplicationTests {

    @Test
    public void contextLoads() {

        String s = ConfigLoader.getValue("zookeeper.serverList");
        System.out.println(s);
    }

}

總結:

    在中大型公司,統一項目配置文件路徑和日誌路徑都是一項政治正確的事。

    統一這些基本規範後,可以避免很多奇奇怪怪的問題。

 

    這樣就滿足了嘛?

    就目前看來這個是基本滿足了需求,略微修改下,打成一個jar包,就可以直接使用了。

 

    但是目前的這種方式,在需要修改配置的時候,還是需要關閉應用然後修改外部配置文件或者命令行參數後,再重啓的。

    有沒有那種可以即時生效的方案呢?答案是:肯定是有的。那就是配置中心。

 

    我們可以引入配置中心,比如開源的Apollo,在上述我們的配置加載中,再加一層,從配置中心中加載配置,就可以實現配置即時生效。

    鑑於時間,筆者就先寫到這。江湖再見!

            

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章