1. 程式人生 > >Springboot @ConditionalOnResource 解決無法讀取外部配置檔案問題

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的啟動畫面,證明沒有問題。