Springboot @ConditionalOnResource 解決無法讀取外部配置檔案問題
前言
最近在開發儲存層基礎中介軟體的過程中,使用到了@ConditionalOnResource這個註解,使用該註解的目的是,註解在Configuration bean上,在其載入之前對指定資源進行校驗,是否存在,如果不存在,丟擲異常;該註解支援傳入多個變數,但是當我們希望原生代碼中不存在配置檔案,依賴配置中心去載入外部的配置檔案啟動時,在註解中傳入一個外部變數,一個本地變數(方便本地開發)時,會丟擲異常,導致專案無法啟動,因此需要解決這個問題。
原因分析
我們首先來分析一下ConditionalOnResource這個註解,原始碼如下:
/** * {@link Conditional} that only matches when the specified resources are on the * classpath. * * @author Dave Syer */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnResourceCondition.class) public @interface ConditionalOnResource { /** * The resources that must be present. * @return the resource paths that must be present. */ String[] resources() default {}; }
我們可以看到,該註解支援resource的入參,是一個數組形式,是可以傳入多個變數的,但是注意看註釋:
that only matches when the specified resources are on the classpath.
好吧,我們貌似看出來一些端倪了,該註解會載入classpath中指定的檔案,但是當我們希望載入外部的配置檔案的時候,為什麼會拋異常呢?我們來看一下這個註解是如何被處理的:
class OnResourceCondition extends SpringBootCondition { private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader(); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attributes = metadata .getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true); ResourceLoader loader = context.getResourceLoader() == null ? this.defaultResourceLoader : context.getResourceLoader(); List<String> locations = new ArrayList<String>(); collectValues(locations, attributes.get("resources")); Assert.isTrue(!locations.isEmpty(), "@ConditionalOnResource annotations must specify at " + "least one resource location"); List<String> missing = new ArrayList<String>(); for (String location : locations) { String resource = context.getEnvironment().resolvePlaceholders(location); if (!loader.getResource(resource).exists()) { missing.add(location); } } if (!missing.isEmpty()) { return ConditionOutcome.noMatch(ConditionMessage .forCondition(ConditionalOnResource.class) .didNotFind("resource", "resources").items(Style.QUOTE, missing)); } return ConditionOutcome .match(ConditionMessage.forCondition(ConditionalOnResource.class) .found("location", "locations").items(locations)); } private void collectValues(List<String> names, List<Object> values) { for (Object value : values) { for (Object item : (Object[]) value) { names.add((String) item); } } } }
這個類是@ConditionalOnResource處理類,getMatchOutcome()方法中去處理邏輯,主要邏輯很簡單,去掃描註解了ConditionalOnResource的類,拿到其resources,分別判斷其路徑下是否存在對應的檔案,如果不存在,丟擲異常。可以看到,它是使用DefaultResourceLoader去載入的檔案,但是這個類只可以載入classpath下的檔案,無法載入外部路徑的檔案,這個就有點尷尬了,明顯無法滿足我的需求。
解決方案
找了找解決方案,發現Spring貌似也沒提供其他合適的註解解決,因此,我想自己去實現一個處理類。
廢話不多說,上原始碼:
@ConditionalOnFile:
/**
* 替換Spring ConditionalOnResource,
* 支援多檔案目錄掃描,如果檔案不存在,跳過繼續掃描
* Created by xuanguangyao on 2018/11/15.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnConditionalOnFile.class)
public @interface ConditionalOnFile {
/**
* The resources that must be present.
* @return the resource paths that must be present.
*/
String[] resources() default {};
}
OnConditionalOnFile:
public class OnConditionalOnFile extends SpringBootCondition {
private final ResourceLoader fileSystemResourceLoader = new FileSystemResourceLoader();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnFile.class.getName(), true);
List<String> locations = new ArrayList<>();
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnFile annotations must specify at "
+ "least one resource location");
for (String location : locations) {
String resourceLocation = context.getEnvironment().resolvePlaceholders(location);
Resource fileResource = this.fileSystemResourceLoader.getResource(resourceLocation);
if (fileResource.exists()) {
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnFile.class)
.found("location", "locations").items(location));
}
}
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnFile.class)
.didNotFind("resource", "resources").items(ConditionMessage.Style.QUOTE, locations));
}
private void collectValues(List<String> names, List<Object> values) {
for (Object value : values) {
for (Object item : (Object[]) value) {
names.add((String) item);
}
}
}
}
OK,我自己實現了一個註解,叫做@ConditionalOnFile,然後自行實現了一個註解的處理類,叫做OnConditionalOnFile,該類需要實現SpringBootCondition,這樣Springboot才會去掃描。
由於原ConditionalOnResource的處理類是使用的DefaultResourceLoader,只可以載入classpath下面的檔案,但是我需要掃描我指定路徑下的外部配置檔案,因此,我使用FileSystemResourceLoader,這個載入器,去載入我的外部配置檔案。
需要注意的是,如果指定外部配置檔案啟動的話,需要在啟動時,指定啟動引數:
--spring.config.location=/myproject/conf/ --spring.profiles.active=production
這樣,才可以順利讀取到外部的配置檔案。
測試
OK,我們測試一下,通過
java -jar myproject.jar --spring.config.location=/myproject/conf/ --spring.profiles.active=production
2018-11-15 20:57:51,131 main ERROR Console contains an invalid element or attribute "encoding"
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.4.1.RELEASE)
[] 2018-11-15 20:57:51 - [INFO] [SpringApplication:665 logStartupProfileInfo] The following profiles are active
All right,看到springboot的啟動畫面,證明沒有問題。