虽然现在springboot提供了多环境的支持,但是通常修改一下配置文件,都需要重新打包。
在开发springboot框架集成时,我遇到一个问题,就是如何让@PropertySource能够“扫描”和加载jar包外面的properties文件。
这样,我就可以随时随地的修改配置文件,不需要重新打包。
最粗暴的方式,就是用–classpath指定这些文件。但是这引入了其他问题,“易于部署”、“与容器无关”,让人棘手。而且这个问题在测试环境、多机房部署、以及与配置中心协作时还是很难巧妙解决,因为这里面涉及到不少的硬性规范、甚至沟通成本。
回到技术的本质,我希望基于spring容器,开发一个兼容性套件,能够扫描jar外部的properties文件,考虑到实施便捷性,我们约定这些properties文件总是位于jar文件的临近目录中。
设计前提
1、文件目录
文件目录就类似于下面的样式。可以看到配置文件是和jar包平行的。
1 2 3 4 5 6 7
| ----application.jar (springboot项目,jarLaucher) | | sample.properties | config/ | | sample.properties
|
2、扫描策略(涉及到覆盖优先级问题)
1)我们约定默认配置文件目录为config,也就是最优先的。其余application.jar同级;相对路径起始位置为jar路径。
2)首先查找./config/sample.properties文件是否存在,如果存在则加载。
3)查找./sample.properties文件是否存在,如果存在则加载。
4)否则,使用classpath加载此文件。
3、开发策略
1)尽可能使用spring机制,即Resource
加载机制,而不适用本地文件或者部署脚本干预等。
2)通过研究,扩展自定义的ResourceLoader
可以达成此目标,但是潜在风险很高,因为springboot、cloud框架内部,对各种Context的支持都有各自的ResourceLoader实现,如果我们再扩展自己的loader会不会导致某些未知问题?于是放弃了此策略。
3)spring提供了ProtocolResolver
机制,用于匹配自定义的文件schema来加载文件;而且不干扰ResourceLoader的机制,最重要的是它会添加到spring环境下的所有的loader中。我们只需要扩展一个ProtocolResolver类,并将它在合适的实际加入到ResourceLoader即可,此后加载properties文件时我们的ProtocolResolver总会被执行。
代码
下面是具体的代码实现。最主要的,就是配置文件解析器的编写。注释很详细,就不多做介绍了。
1、XPathProtocolResolver.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| 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;
public class XPathProtocolResolver implements ProtocolResolver {
private static final String X_PATH_OUTSIDE_PREFIX = "::";
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; }
private String path(String location) { return location.substring(2); } }
|
2、ResourceLoaderPostProcessor.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.env.Environment;
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
1 2
| org.springframework.context.ApplicationContextInitializer=\ com.github.xjjdog.commons.spring.io.ResourceLoaderPostProcessor
|
PropertyConfiguration.java (springboot环境下,properties加载器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @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组件,增加了新的功能。
End
SpringBoot通过设置”spring.profiles.active”可以指定不同的环境,但是需求总是多变的。比如本文的配置需求,可能就是某个公司蛋疼的约定。
SpringBoot提供了多种扩展方式来支持这些自定义的操作,这也是魅力所在。没有什么,不是开发一个spring boot starter不能解决的。