雖然現在springboot提供了多環境的支援,但是通常修改一下配置檔案,都需要重新打包。
在開發springboot框架整合時,我遇到一個問題,就是如何讓@PropertySource能夠“掃描”和載入jar包外面的properties檔案。
這樣,我就可以隨時隨地的修改配置檔案,不需要重新打包。
最粗暴的方式,就是用--classpath指定這些檔案。但是這引入了其他問題,“易於部署”、“與容器無關”,讓人棘手。而且這個問題在測試環境、多機房部署、以及與配置中心協作時還是很難巧妙解決,因為這裡面涉及到不少的硬性規範、甚至溝通成本。
回到技術的本質,我希望基於spring容器,開發一個相容性套件,能夠掃描jar外部的properties檔案,考慮到實施便捷性,我們約定這些properties檔案總是位於jar檔案的臨近目錄中。
設計前提1、檔案目錄
檔案目錄就類似於下面的樣式。可以看到配置檔案是和jar包平行的。
----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
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元件,增加了新的功能。
EndSpringBoot通過設定"spring.profiles.active"可以指定不同的環境,但是需求總是多變的。比如本文的配置需求,可能就是某個公司蛋疼的約定。
SpringBoot提供了多種擴充套件方式來支援這些自定義的操作,這也是魅力所在。沒有什麼,不是開發一個spring boot starter不能解決的。