Spring会默认加载application.properties文件,我们一般可以将配置写在此处。这基本可以满足我们的常用demo项目使用。
但是在实际项目开发中,我们会将配置文件外置,这样在我们需要修改配置的时候就不用将项目重新打包部署了。
下面我们来看一下实际项目开发的需求。
举给例子:
1.我们希望项目启动后会加载内部配置文件(统一命名为env.properties)
2.如果有外置配置文件的话(路径设置为/envconfig/${app.name}/env.properties),则加载外置配置文件,并覆盖内部配置文件的相同key的项
3.如果在项目启动时候指定了命令行参数,则该参数级别最高,可以覆盖外置配置文件相同key的项
以上这个需求,我们用目前Spring的加载配置的方式就有点难以完成了。所以这时候我们需要自定义加载方式。
笔者新建了一个SpringBoot项目,maven基本配置如下:
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
org.apache.commons
commons-lang3
3.4
/**
* 客户端自定义加载配置
*
* @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());
}
}
org.springframework.boot.env.EnvironmentPostProcessor=com.xw.study.configload.processor.CustomerConfigLoadProcessor
以上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:";
/**
* Load the properties from provided file.
* Currently it supports reading from classpath file or local file.
*
* @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 list = new ArrayList<>();
try {
Enumeration 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() {
}
}
代码不算复杂,笔者不再详述。
根据以上的加载顺序,就可以实现 命令行 > 外部配置文件 > 内部配置文件的需求。
这个比较简单了,用户可自行测试
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,在上述我们的配置加载中,再加一层,从配置中心中加载配置,就可以实现配置即时生效。
鉴于时间,笔者就先写到这。江湖再见!