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,在上述我们的配置加载中,再加一层,从配置中心中加载配置,就可以实现配置即时生效。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。