前言:
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,在上述我們的配置加載中,再加一層,從配置中心中加載配置,就可以實現配置即時生效。
鑑於時間,筆者就先寫到這。江湖再見!