首頁>技術>

雖然現在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元件,增加了新的功能。

End

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

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

  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 前端熱門技術方向整理(全)