原创: 小姐姐味道(微信公众号ID: xjjdog),欢迎分享,转载请保留出处。
虽然现在springboot提供了多环境的支持,但是通常修改一下配置文件,都需要重新打包。
在开发springboot框架集成时,我遇到一个问题,就是如何让 @PropertySource
能够“扫描”和加载jar包 外面
的properties文件。
这样,我就可以随时随地的修改配置文件,不需要重新打包。
最粗暴的方式,就是用—classpath指定这些文件。但是这引入了其他问题,“易于部署”、“与容器无关”,让人棘手。而且这个问题在测试环境、多机房部署、以及与配置中心协作时还是很难巧妙解决,因为这里面涉及到不少的硬性规范、甚至沟通成本。
回到技术的本质,我希望基于spring容器,开发一个兼容性套件,能够扫描jar外部的properties文件,考虑到实施便捷性,我们约定这些properties文件总是位于jar文件的临近目录中。
文件目录就类似于下面的样式。可以看到配置文件是和jar包平行的。
----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总会被执行。
下面是具体的代码实现。最主要的,就是配置文件解析器的编写。注释很详细,就不多做介绍了。
1、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> * 例如:* 1、@PropertySource("::a.property") * 查找路径为:./config/a.property,./a.property,如果找不到则返回null,路径相对于app.jar * 2、@PropertySource("::x/a.property") * 查找路径为:./config/x/a.property,./x/a.property,路径相对于app.jar * 3、@PropertySource("*:a.property") * 查找路径为:./config/a.property,./a.property,CLASSPATH:/config/a.property,CLASSPATH:/a.property * 4、@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); } }
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了。没错,就是要做一个。
3、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,欢迎添加好友,进一步交流。
JAVA后端知识索引
1、
必看!java后端,亮剑诛仙2、学完这100多技术,能当架构师么?
两篇索引,吊打全网,也是小姐姐味道系统化文章展开路径,定期梳理
Linux
Linux有“最常用”和“荒岛余生”两个系列,最爱的编辑器当然是 vim。
我整理了一份《最有用系列,百页精华》pdf,想要可以加我微信:xjjdog0。
Linux上,最常用的一批命令解析
csdn发布首日,即获1k+赞,点赞率1/8
最常用系列↓(更多见文末)
1、最常用的一套“Vim“技巧
2、 最常用的一套“Sed“技巧
3、 最常用的一套“AWK“技巧
基础架构
基础架构的范围,大多数是一些整体性的方案,但又比较深入底层原理。这方面的工作有两个特点:思路要全,内容要专。
这次要是讲不明白Spring Cloud核心组件,那我就白编这故事了
有比较精细俏皮的讲解
微服务不是全部,只是特定领域的子集也有大而全的解决方案
解决方案:
1、 服务端开发学习路径图,心疼小哥哥们
2、 这么多监控组件,总有一款适合你
3、 “分库分表” ?选型和流程要慎重,否则会失控
4、 使用Netty,我们到底在开发些什么?
5、 ”MySQL官方驱动“主从分离的神秘面纱
6、 这可能是最中肯的Redis规范了
7、 发布系统有那么难么?
8、 也浅谈下分布式存储要点
7、 希望一个数据同步,包治百病
8、 如何使用postgis做一个高可用的附近的人服务?
9、 那些需要自己开发的安全需求
10、WebSocket协议 8 问
11、 JAVA多线程使用场景和注意事项简版
12、 上厅房,下厨房,ElasticSearch有的忙
13、 分布式消息系统,设计要点。画龙画虎难画骨
14、Kafka基础知识索引
15、 与亲生的Redis Cluster,来一次亲密接触
高并发相关(未完结):
1、高并发之限流,到底限的什么鬼
2、信号量限流,高并发场景不得不说的秘密
3、没有预热,不叫高并发,叫并发高
4、这样的高可用,我不要!细节深入: