SpringBoot 如何加載 jar 包外面的配置文件?

轉自:小姐姐味道

雖然現在springboot提供了多環境的支持,但是通常修改一下配置文件,都需要重新打包。


在開發springboot框架集成時,我遇到一個問題,就是如何讓@PropertySource能夠“掃描”和加載jar包外面的properties文件。


這樣,我就可以隨時隨地的修改配置文件,不需要重新打包。


最粗暴的方式,就是用—classpath指定這些文件。但是這引入了其他問題,“易於部署”、“與容器無關”,讓人棘手。而且這個問題在測試環境、多機房部署、以及與配置中心協作時還是很難巧妙解決,因為這裡面涉及到不少的硬性規範、甚至溝通成本。


回到技術的本質,我希望基於spring容器,開發一個兼容性套件,能夠掃描jar外部的properties文件,考慮到實施便捷性,我們約定這些properties文件總是位於jar文件的臨近目錄中。


設計前提


1、文件目錄


文件目錄就類似於下面的樣式。可以看到配置文件是和jar包平行的。


<code>----application.jar  (springboot項目,jarLaucher)     |     | sample.properties     | config/             |             | sample.properties/<code>


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


<code>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 * 

* 例如:* 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 *

* 如果指定了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); }}/<string>/<string>/<string>/<string>/<string>/<string>

/<code>


2、ResourceLoaderPostProcessor.java


<code>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;    }}/<configurableapplicationcontext>/<code>


加上spring.factories,我們越來越像是在做一個starter了。沒錯,就是要做一個。


3、spring.factories


<code>org.springframework.context.ApplicationContextInitializer=\\com.github.xjjdog.commons.spring.io.ResourceLoaderPostProcessor/<code>


PropertyConfiguration.java (springboot環境下,properties加載器)


<code>@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();    }}/<code>


這樣,我們的自定義加載器就完成了。我們也為SpringBoot組件,增加了新的功能。


End


SpringBoot通過設置”spring.profiles.active”可以指定不同的環境,但是需求總是多變的。比如本文的配置需求,可能就是某個公司蛋疼的約定。


SpringBoot提供了多種擴展方式來支持這些自定義的操作,這也是魅力所在。沒有什麼,不是開發一個spring boot starter不能解決的。


分享到:


相關文章: