原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。java
虽然如今springboot提供了多环境的支持,可是一般修改一下配置文件,都须要从新打包。git
在开发springboot框架集成时,我遇到一个问题,就是如何让@PropertySource
可以“扫描”和加载jar包外面
的properties文件。程序员
这样,我就能够随时随地的修改配置文件,不须要从新打包。github
最粗暴的方式,就是用--classpath指定这些文件。可是这引入了其余问题,“易于部署”、“与容器无关”,让人棘手。并且这个问题在测试环境、多机房部署、以及与配置中心协做时仍是很难巧妙解决,由于这里面涉及到很多的硬性规范、甚至沟通成本。spring
回到技术的本质,我但愿基于spring容器,开发一个兼容性套件,可以扫描jar外部的properties文件,考虑到实施便捷性,咱们约定这些properties文件老是位于jar文件的临近目录中。api
一、文件目录springboot
文件目录就相似于下面的样式。能够看到配置文件是和jar包平行的。bash
----application.jar (springboot项目,jarLaucher)
|
| sample.properties
| config/
|
| sample.properties
复制代码
二、扫描策略(涉及到覆盖优先级问题)微信
1)咱们约定默认配置文件目录为config,也就是最优先的。其他application.jar同级;相对路径起始位置为jar路径。架构
2)首先查找./config/sample.properties文件是否存在,若是存在则加载。
3)查找./sample.properties文件是否存在,若是存在则加载。
4)不然,使用classpath加载此文件。
三、开发策略
1)尽量使用spring机制,即Resource
加载机制,而不适用本地文件或者部署脚本干预等。
2)经过研究,扩展自定义的ResourceLoader
能够达成此目标,可是潜在风险很高,由于springboot、cloud框架内部,对各类Context的支持都有各自的ResourceLoader实现,若是咱们再扩展本身的loader会不会致使某些未知问题?因而放弃了此策略。
3)spring提供了ProtocolResolver
机制,用于匹配自定义的文件schema来加载文件;并且不干扰ResourceLoader的机制,最重要的是它会添加到spring环境下的全部的loader中。咱们只须要扩展一个ProtocolResolver类,并将它在合适的实际加入到ResourceLoader便可,此后加载properties文件时咱们的ProtocolResolver总会被执行。
下面是具体的代码实现。最主要的,就是配置文件解析器的编写。注释很详细,就很少作介绍了。
一、XPathProtocolResolver.java
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ResourceUtils;
import java.util.Collection;
import java.util.LinkedHashSet;
/**
* 用于加载jar外部的properties文件,扩展classpath : xjjdog
* -- app.jar
* -- config/a.property INSIDE order=3
* -- a.property INSIDE order=4
* -- config/a.property OUTSIDE order=1
* -- a.property OUTSIDE order=2
* <p>
* 例如:
* 一、@PropertySource("::a.property")
* 查找路径为:./config/a.property,./a.property,若是找不到则返回null,路径相对于app.jar
* 二、@PropertySource("::x/a.property")
* 查找路径为:./config/x/a.property,./x/a.property,路径相对于app.jar
* 三、@PropertySource("*:a.property")
* 查找路径为:./config/a.property,./a.property,CLASSPATH:/config/a.property,CLASSPATH:/a.property
* 四、@PropertySource("*:x/a.property")
* 查找路径为:./config/x/a.property,./x/a.property,CLASSPATH:/config/x/a.property,CLASSPATH:/x/a.property
* <p>
* 若是指定了customConfigPath,上述路径中的/config则会被替换
*
* @author xjjdog
**/
public class XPathProtocolResolver implements ProtocolResolver {
/**
* 查找OUTSIDE的配置路径,若是找不到,则返回null
*/
private static final String X_PATH_OUTSIDE_PREFIX = "::";
/**
* 查找OUTSIDE 和inside,其中inside将会转换为CLASS_PATH
*/
private static final String X_PATH_GLOBAL_PREFIX = "*:";
private String customConfigPath;
public XPathProtocolResolver(String configPath) {
this.customConfigPath = configPath;
}
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (!location.startsWith(X_PATH_OUTSIDE_PREFIX) && !location.startsWith(X_PATH_GLOBAL_PREFIX)) {
return null;
}
String real = path(location);
Collection<String> fileLocations = searchLocationsForFile(real);
for (String path : fileLocations) {
Resource resource = resourceLoader.getResource(path);
if (resource != null && resource.exists()) {
return resource;
}
}
boolean global = location.startsWith(X_PATH_GLOBAL_PREFIX);
if (!global) {
return null;
}
Collection<String> classpathLocations = searchLocationsForClasspath(real);
for (String path : classpathLocations) {
Resource resource = resourceLoader.getResource(path);
if (resource != null && resource.exists()) {
return resource;
}
}
return resourceLoader.getResource(real);
}
private Collection<String> searchLocationsForFile(String location) {
Collection<String> locations = new LinkedHashSet<>();
String _location = shaping(location);
if (customConfigPath != null) {
String prefix = ResourceUtils.FILE_URL_PREFIX + customConfigPath;
if (!customConfigPath.endsWith("/")) {
locations.add(prefix + "/" + _location);
} else {
locations.add(prefix + _location);
}
} else {
locations.add(ResourceUtils.FILE_URL_PREFIX + "./config/" + _location);
}
locations.add(ResourceUtils.FILE_URL_PREFIX + "./" + _location);
return locations;
}
private Collection<String> searchLocationsForClasspath(String location) {
Collection<String> locations = new LinkedHashSet<>();
String _location = shaping(location);
if (customConfigPath != null) {
String prefix = ResourceUtils.CLASSPATH_URL_PREFIX + customConfigPath;
if (!customConfigPath.endsWith("/")) {
locations.add(prefix + "/" + _location);
} else {
locations.add(prefix + _location);
}
} else {
locations.add(ResourceUtils.CLASSPATH_URL_PREFIX + "/config/" + _location);
}
locations.add(ResourceUtils.CLASSPATH_URL_PREFIX + "/" + _location);
return locations;
}
private String shaping(String location) {
if (location.startsWith("./")) {
return location.substring(2);
}
if (location.startsWith("/")) {
return location.substring(1);
}
return location;
}
/**
* remove protocol
*
* @param location
* @return
*/
private String path(String location) {
return location.substring(2);
}
}
复制代码
二、ResourceLoaderPostProcessor.java
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
/**
* @author xjjdog
* 调整优化环境变量,对于boot框架会默认覆盖一些环境变量,此时咱们须要在processor中执行
* 咱们再也不须要使用单独的yml文件来解决此问题。原则:
* 1)全部设置为系统属性的,初衷为"对系统管理员可见"、"对外部接入组件可见"(好比starter或者日志组件等)
* 2)对设置为lastSource,表示"当用户没有经过yml"配置选项时的默认值--担保策略。
**/
public class ResourceLoaderPostProcessor implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
Environment environment = applicationContext.getEnvironment();
String configPath = environment.getProperty("CONF_PATH");
if (configPath == null) {
configPath = environment.getProperty("config.path");
}
applicationContext.addProtocolResolver(new XPathProtocolResolver(configPath));
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 100;
}
}
复制代码
加上spring.factories,咱们愈来愈像是在作一个starter了。没错,就是要作一个。
三、spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.github.xjjdog.commons.spring.io.ResourceLoaderPostProcessor
复制代码
PropertyConfiguration.java (springboot环境下,properties加载器)
@Configuration
@PropertySources(
{
@PropertySource("*:login.properties"),
@PropertySource("*:ldap.properties")
}
)
public class PropertyConfiguration {
@Bean
@ConfigurationProperties(prefix = "login")
public LoginProperties loginProperties() {
return new LoginProperties();
}
@Bean
@ConfigurationProperties(prefix = "ldap")
public LdapProperties ldapProperties() {
return new LdapProperties();
}
}
复制代码
这样,咱们的自定义加载器就完成了。咱们也为SpringBoot组件,增长了新的功能。
SpringBoot经过设置"spring.profiles.active"能够指定不一样的环境,可是需求老是多变的。好比本文的配置需求,可能就是某个公司蛋疼的约定。
SpringBoot提供了多种扩展方式来支持这些自定义的操做,这也是魅力所在。没有什么,不是开发一个spring boot starter不能解决的。
做者简介:小姐姐味道 (xjjdog),一个不容许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不同的味道。个人我的微信xjjdog0,欢迎添加好友,进一步交流。