Spring如何自定義加載配置文件(分層次加載)
前言
Spring會默認加載application.properties文件,我們一般可以將配置寫在此處。這基本可以滿足我們的常用demo項目使用。
但是在實際項目開發(fā)中,我們會將配置文件外置,這樣在我們需要修改配置的時候就不用將項目重新打包部署了。
下面我們來看一下實際項目開發(fā)的需求。
針對配置分層次加載的需求
舉給例子
1.我們希望項目啟動后會加載內(nèi)部配置文件(統(tǒng)一命名為env.properties)
2.如果有外置配置文件的話(路徑設(shè)置為/envconfig/${app.name}/env.properties),則加載外置配置文件,并覆蓋內(nèi)部配置文件的相同key的項
3.如果在項目啟動時候指定了命令行參數(shù),則該參數(shù)級別最高,可以覆蓋外置配置文件相同key的項
以上這個需求,我們用目前Spring的加載配置的方式就有點難以完成了。
所以這時候我們需要自定義加載方式。
環(huán)境準備
筆者新建了一個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/下創(chuàng)建spring.factories文件
并添加
org.springframework.boot.env.EnvironmentPostProcessor=com.xw.study.configload.processor.CustomerConfigLoadProcessor? ??
3.實現(xiàn)配置加載邏輯
以上spring environment框架搭建好之后,在項目啟動時候就會去加載ConfigLoader對應的Properties信息到當前運行環(huán)境中。
下面就來看下加載邏輯:
/** ?* 配置加載器 ?* ?* @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.加載項目內(nèi)置classpath:env.properties ? ? ?* 2.加載外部配置文件env.properties(會給定一個默認路徑) ? ? ?* 3.加載JVM命令行參數(shù) ? ? ?*/ ? ? private void loadProperties() { ? ? ? ? loadLocalProperties(); ? ? ? ? loadExtProperties(); ? ? ? ? loadSystemEnvProperties(); ? ? } ? ? /** ? ? ?* 加載JVM命令行參數(shù)、Environment參數(shù) ? ? ?*/ ? ? private void loadSystemEnvProperties() { ? ? ? ? prop.putAll(System.getenv()); ? ? ? ? prop.putAll(System.getProperties()); ? ? } ? ? /** ? ? ?* 加載外部配置文件env.properties(會給定一個默認路徑) ? ? ?* 筆者所在公司,會根據(jù)不同的項目名,統(tǒng)一路徑設(shè)置為 ? ? ?* /envconfig/{app.name}/env.properties ? ? ?*/ ? ? private void loadExtProperties() { ? ? ? ? // 獲取全路徑 ? ? ? ? // 所以需要首先在內(nèi)部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); ? ? } ? ? /** ? ? ?* 加載項目內(nèi)置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() { ? ? } }
代碼不算復雜,筆者不再詳述。
根據(jù)以上的加載順序,就可以實現(xiàn) 命令行 > 外部配置文件 > 內(nèi)部配置文件的需求。
4.測試
這個比較簡單了,用戶可自行測試
1)只有內(nèi)部配置文件
在/resources下創(chuàng)建env.properties文件
2)內(nèi)部配置文件、外部配置文件均存在
滿足1)的同時(注意有一個必備項為app.name,筆者自定義為configload),在本地磁盤創(chuàng)建/envconfig/configload/env.properties文件
3)添加命令行參數(shù)
在滿足2)的同時,在啟動行添加參數(shù)(-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); ? ? } }
總結(jié)
在中大型公司,統(tǒng)一項目配置文件路徑和日志路徑都是一項政治正確的事。
統(tǒng)一這些基本規(guī)范后,可以避免很多奇奇怪怪的問題。
這樣就滿足了嘛?
就目前看來這個是基本滿足了需求,略微修改下,打成一個jar包,就可以直接使用了。
但是目前的這種方式,在需要修改配置的時候,還是需要關(guān)閉應用然后修改外部配置文件或者命令行參數(shù)后,再重啟的。
有沒有那種可以即時生效的方案呢?答案是:肯定是有的。那就是配置中心。
我們可以引入配置中心,比如開源的Apollo,在上述我們的配置加載中,再加一層,從配置中心中加載配置,就可以實現(xiàn)配置即時生效。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
九個動畫組圖輪播總結(jié)全棧數(shù)據(jù)結(jié)構(gòu)數(shù)組鏈表
數(shù)據(jù)結(jié)構(gòu)和算法是密不可分的,兩者往往是相輔相成的存在,所以在學習數(shù)據(jù)結(jié)構(gòu)過程中,不免會遇到各種算法,數(shù)據(jù)結(jié)構(gòu)常用操作一般為:增刪改查?;旧纤械臄?shù)據(jù)結(jié)構(gòu)都是圍繞這幾個操作進行展開,本文用九張動圖來闡述先進后出的數(shù)據(jù)結(jié)構(gòu)2021-08-08springboot 如何解決static調(diào)用service為null
這篇文章主要介紹了springboot 如何解決static調(diào)用service為null的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06Springboot+Vue+shiro實現(xiàn)前后端分離、權(quán)限控制的示例代碼
這篇文章主要介紹了Springboot+Vue+shiro實現(xiàn)前后端分離、權(quán)限控制的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07java http連接池的實現(xiàn)方式(帶有失敗重試等高級功能)
這篇文章主要介紹了java http連接池的實現(xiàn)方式(帶有失敗重試等高級功能),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04Springboot mybatis plus druid多數(shù)據(jù)源解決方案 dynamic-datasource的使用詳
這篇文章主要介紹了Springboot mybatis plus druid多數(shù)據(jù)源解決方案 dynamic-datasource的使用,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11SpringMVC結(jié)合ajaxfileupload.js實現(xiàn)文件無刷新上傳
這篇文章主要介紹了SpringMVC結(jié)合ajaxfileupload.js實現(xiàn)文件無刷新上傳,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10Caused by: java.lang.ClassNotFoundException: org.apache.comm
這篇文章主要介紹了Caused by: java.lang.ClassNotFoundException: org.objectweb.asm.Type異常,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07