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

原創:小姐姐味道(微信公衆號ID:xjjdog),歡迎分享,轉載請保留出處。java

雖然如今springboot提供了多環境的支持,可是一般修改一下配置文件,都須要從新打包。git

在開發springboot框架集成時,我遇到一個問題,就是如何讓@PropertySource可以「掃描」和加載jar包外面的properties文件。程序員

這樣,我就能夠隨時隨地的修改配置文件,不須要從新打包。github

最粗暴的方式,就是用--classpath指定這些文件。可是這引入了其餘問題,「易於部署」、「與容器無關」,讓人棘手。並且這個問題在測試環境、多機房部署、以及與配置中心協做時仍是很難巧妙解決,由於這裏面涉及到很多的硬性規範、甚至溝通成本。spring

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

設計前提

一、文件目錄springboot

文件目錄就相似於下面的樣式。能夠看到配置文件是和jar包平行的。bash

----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總會被執行。

代碼

下面是具體的代碼實現。最主要的,就是配置文件解析器的編寫。註釋很詳細,就很少作介紹了。

一、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> 
 * 例如: 
 * 一、@PropertySource("::a.property") 
 * 查找路徑爲:./config/a.property,./a.property,若是找不到則返回null,路徑相對於app.jar 
 * 二、@PropertySource("::x/a.property") 
 * 查找路徑爲:./config/x/a.property,./x/a.property,路徑相對於app.jar 
 * 三、@PropertySource("*:a.property") 
 * 查找路徑爲:./config/a.property,./a.property,CLASSPATH:/config/a.property,CLASSPATH:/a.property 
 * 四、@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);  
    }  
}  
複製代碼

二、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了。沒錯,就是要作一個。

三、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不能解決的。

做者簡介:小姐姐味道 (xjjdog),一個不容許程序員走彎路的公衆號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不同的味道。個人我的微信xjjdog0,歡迎添加好友,​進一步交流。​

相關文章
相關標籤/搜索