首頁>技術>

轉自:小姐姐味道

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

在開發springboot框架整合時,我遇到一個問題,就是如何讓@PropertySource能夠“掃描”和載入jar包外面的properties檔案。

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

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

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

設計前提

1、檔案目錄

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

----application.jar  (springboot專案,jarLaucher)     |     | sample.properties     | config/             |             | sample.properties

2、掃描策略(涉及到覆蓋優先順序問題)

我們約定預設配置檔案目錄為config,也就是最優先的。其餘application.jar同級;相對路徑起始位置為jar路徑。首先查詢./config/sample.properties檔案是否存在,如果存在則載入。查詢./sample.properties檔案是否存在,如果存在則載入。否則,使用classpath載入此檔案。

3、開發策略

儘可能使用spring機制,即Resource載入機制,而不適用本地檔案或者部署指令碼干預等。通過研究,擴充套件自定義的ResourceLoader可以達成此目標,但是潛在風險很高,因為springboot、cloud框架內部,對各種Context的支援都有各自的ResourceLoader實現,如果我們再擴充套件自己的loader會不會導致某些未知問題?於是放棄了此策略。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元件,增加了新的功能。

End

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

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

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Spring Boot 中通過 CORS 解決跨域問題