天天看點

SpringBoot系列-配置解析

注:本文基于 SpringBoot 2.1.11 版本

說到配置,你能想到的是什麼?

在日常的開發和運維過程中,可以說配置都是及其重要的,因為它可能影響到應用的正常啟動或者正常運作。相信在之前 Spring xml 時代,很多人都會被一堆 xml 配置折騰的夠嗆,除此之外,還有像資料庫連接配接配置、緩存配置、注冊中心配置、消息配置等等,這些相信大家都不會陌生。

配置對于開發人員或者運維人員來說可以比喻成一把”鑰匙“,可以通過這把”鑰匙“讓我們的程式 run 起來,可以通過這把 ”鑰匙“ 開啟或者關閉應用程式的某一個功能。那麼為什麼會需要配置,對于一個應用來說,配置的意義又是什麼呢?

配置對于架構元件和應用程式的意義

配置對于架構元件和應用程式的意義是什麼?我的了解是可以讓架構元件和應用程式變得靈活,通過配置可以使得一個架構元件或者一個應用程式在不需要做任何自身代碼變更的情況下跑在不同的環境、不同的場景下。例如 Dubbo ,使用者可以通過配置使得 Dubbo 将服務注冊到不同的注冊中心,nacos、zookeeper、SOFARegistry 等等;再比如,我有一個應用程式,在 dev 環境和生産環境需要連接配接不同的資料庫,但是我又不想去在代碼裡面去做修改來适配不同的環境,那麼同樣我也可以使用配置的方式來做控制。配置可以讓架構元件和應用程式變得靈活、不強耦合在某一個場景或者環境下,它可以有很多種存在形态,如常見的是存在檔案中、配置中心中、系統環境變量中,對于 JAVA 程式來說還可以是指令行參數或者 -D 參數。可以說任何優秀的架構或者應用,都離不開配置。

那麼作為 Java 語言生态裡面最優秀的架構, Spring 是如何管理和使用配置的呢?本篇将以 SpringBoot 中的配置為切入點,來進行詳細的剖析。

SpringBoot 中的配置

Spring Boot 官方文章中使用了單獨的章節和大量的篇幅對配置進行了描述,可以見得,配置對于 SpringBoot 來說,是相當重要的。Spring Boot 允許使用者将配置外部化,以便可以在不同的環境中使用相同的應用程式代碼,使用者可以使用 properties 檔案、YAML 檔案、環境變量和指令行參數來具體化配置。屬性值可以通過使用 @Value 注釋直接注入 bean,可以通過 Spring 的環境抽象通路,也可以通過 @ConfigurationProperties 綁定到結構化對象。

在日常的開發中,對于 SpringBoot 中的配置,可能直接想到的就是 application.properties,實際上,從 SpringBoot 官方文檔可以看到,SpringBoot 擷取配置的方式有多達 17 種;同時 Spring Boot 也提供了一種非常特殊的 PropertyOrder,來允許使用者可以在适當的場景下覆寫某些屬性值,下面就是官方文檔中描述的屬性優先加載順序:

  • 1.在主目錄(當 devtools 被激活,則為 ~/.spring-boot-devtools.properties )中的 Devtools 全局設定屬性。
  • 2.在測試中使用到的 @TestPropertySource 注解。
  • 3.在測試中使用到的 properties 屬性,可以是 @SpringBootTest 和用于測試應用程式某部分的測試注解。
  • 4.指令行參數。
  • 5.來自 SPRING_APPLICATION_JSON 的屬性(嵌入在環境變量或者系統屬性【system propert】中的内聯 JSON)
  • 6.ServletConfig 初始化參數。
  • 7.ServletContext 初始化參數。
  • 8.來自 java:comp/env 的 JNDI 屬性。
  • 9.Java 系統屬性(System.getProperties())。
  • 10.作業系統環境變量。
  • 11.隻有 random.* 屬性的 RandomValuePropertySource。
  • 12.在已打包的 fatjar 外部的指定 profile 的應用屬性檔案(application-{profile}.properties 和 YAML 變量)。
  • 13.在已打包的 fatjar 内部的指定 profile 的應用屬性檔案(application-{profile}.properties 和 YAML 變量)。
  • 14.在已打包的 fatjar 外部的應用屬性檔案(application.properties 和 YAML 變量)。
  • 15.在已打包的 fatjar 内部的應用屬性檔案(application.properties 和 YAML 變量)。
  • 16.在 @Configuration 類上的 @PropertySource 注解。
  • 17.預設屬性(使用 SpringApplication.setDefaultProperties 指定)。

相信絕大多數都是你不曾用過的,不用糾結,其實用不到也很正常,但是我們還是需要能夠知道它提供的方式有哪些,以便于在适當的場景下掏出來鎮樓!

Spring 中對于配置最終都是交給 Environment 對象來管理,也就是我們常說的 Spring 環境。比如可以通過以下方式從 Environment 中擷取配置值:

ConfigurableEnvironment environment = context.getEnvironment();
environment.getProperty("key");           

複制

那麼 Environment 是如何被建構的呢?Environment 與配置的關系又是什麼?

Environment 建構

Environment 的建構發生在 prepareEnvironment 中,關于 SpringBoot 啟動過程想了解更多,可以參考這篇 SpringBoot系列-啟動過程分析。

private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		switch (this.webApplicationType) {
        // 标準的 web 應用
		case SERVLET:
			return new StandardServletEnvironment();
        // webflux 應用
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
        // 非web應用
		default:
			return new StandardEnvironment();
		}
	}           

複制

本篇基于非 web 應用分析,所有主要圍繞 StandardEnvironment 這個類展開分析。

Environment 類繼承結構體系:

SpringBoot系列-配置解析

systemProperties & systemEnvironment

在建構 StandardEnvironment 對象的過程中,會初始化 systemProperties & systemEnvironment 兩個 PropertySource。其觸發時機是在其父類 AbstractEnvironment 的構造函數中。customizePropertySources 方法在 AbstractEnvironment 中并沒有具體的實作,其依賴子類完成,如下:

public AbstractEnvironment() {
    customizePropertySources(this.propertySources);
}

// 子類 StandardEnvironment 中的實作邏輯
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    // 建構 systemProperties 配置
    propertySources.addLast(
            new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    // // 建構 systemEnvironment 配置
    propertySources.addLast(
            new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}           

複制

以我本機為例,來分别看下 systemProperties 和 systemEnvironment 主要是哪些東西

  • systemProperties
SpringBoot系列-配置解析
  • systemEnvironment
SpringBoot系列-配置解析

defaultProperties & commandLineArgs

在建構完預設的 Environment 完成之後就是配置 Environment ,這裡主要就包括預設的 defaultProperties 和指令行參數兩個部分。defaultProperties 可以通過以下方式設定:

Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("defaultKey","defaultValue");
SpringApplication springApplication = new SpringApplication(BootStrap.class);
springApplication.setDefaultProperties(defaultProperties);
springApplication.run(args);           

複制

配置 defaultProperties 和指令行參數過程的代碼如下:

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    MutablePropertySources sources = environment.getPropertySources();
    // 如果 springApplication 設定了則建構 defaultProperties,沒有就算了
    if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
        sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
    }
    // 指令行參數
    if (this.addCommandLineProperties && args.length > 0) {
        // PropertySource 名為 commandLineArgs
        String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
        if (sources.contains(name)) {
            PropertySource<?> source = sources.get(name);
            CompositePropertySource composite = new CompositePropertySource(name);
            composite.addPropertySource(
                    new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
            composite.addPropertySource(source);
            sources.replace(name, composite);
        }
        else {
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }
}           

複制

SpringBoot 打成 fatjar 包後通過指令行傳入的參數 包括以下 3 種實作方式

  • java -jar xxx.jar a b c : 通過 main 方法的參數擷取,即 args
  • java -jar xxx.jar -Dp1=a -Dp2=b -Dp3=c : -D 參數方式,會被設定到系統參數中
  • java -jar xxx.jar --p1=a --p2=b --p3=c : SpringBoot 規範方式,可以通過 @Value("${p1}") 擷取

配置 Profiles

為 application enviroment 配置哪些配置檔案是 active 的(或者預設情況下是 active)。在配置檔案處理期間,可以通過 spring.profiles.active 配置屬性來激活其他配置檔案。主要包括兩種:

  • 通過 spring.profiles.active
protected Set<String> doGetActiveProfiles() {
    synchronized (this.activeProfiles) {
        if (this.activeProfiles.isEmpty()) {
            // 擷取 spring.profiles.active 配置值
            // 如:spring.profiles.active=local ,profiles 為 local
            // 如:spring.profiles.active=local,dev ,profiles 為 local,dev
            String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
            if (StringUtils.hasText(profiles)) {
                // 按 ,分割成 String[] 數組
                setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
                        StringUtils.trimAllWhitespace(profiles)));
            }
        }
        // 傳回,這裡還沒有解析和 merge 配置
        return this.activeProfiles;
    }
}           

複制

  • 配置通過 SpringApplication 對象 setAdditionalProfiles 配置
SpringApplication springApplication = new SpringApplication(BootStrap.class);
// 設定 dev
springApplication.setAdditionalProfiles("dev");
springApplication.run(args);           

複制

以上兩種方式設定的 profiles 會作為最後生效的 activeProfiles。

configurationProperties

将 ConfigurationPropertySource 支援附加到指定的 Environment。将 Environment 管理的每個 PropertySource 調整為 ConfigurationPropertySource 類型,并允許 PropertySourcesPropertyResolver 使用 ConfigurationPropertyName 調用解析。附加的解析器将動态跟蹤任何來自基礎環境屬性源的添加或删除(這個也是 SpringCloud Config 的底層支援原理)。

public static void attach(Environment environment) {
    // 類型檢查
    Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
    MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
    // 擷取名為 configurationProperties 的 PropertySource
    PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
    // 如果存在先移除,保證每次都是最新的 PropertySource
    if (attached != null && attached.getSource() != sources) {
        sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
        attached = null;
    }
    if (attached == null) {
        // 重新将名為 configurationProperties 的 PropertySource 放到屬性源中
        sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
                new SpringConfigurationPropertySources(sources)));
    }
}           

複制

綁定 Environment 到 SpringApplication

在 Spring Boot 2.0 中,用于綁定 Environment 屬性的機制 @ConfigurationProperties 已經完全徹底修改; 是以相信很多人在遷移 SpringBoot 從 1.x 到 2.x 系列時,或者或少都會踩這塊的坑。

新的 API 可以使得 @ConfigurationProperties 直接在你自己的代碼之外使用。綁定規則可以參考:Relaxed-Binding-2.0。這裡簡單示範下:

// 綁定 CustomProp
List<CustomProp> props = Binder.get(run.getEnvironment())
                .bind("glmapper.property", Bindable.listOf(CustomProp.class))
                .orElseThrow(IllegalStateException::new);
// 配置類
@ConfigurationProperties(prefix = "glmapper.property")
public class CustomProp {
    private String name;
    private int age;
    // 省略 get&set
}           

複制

屬性配置:

glmapper:
  property:
    - name: glmapper
      age: 26
    - name: slg
      age: 26           

複制

從上面整個建構過程來看,Enviroment 對象建構實際就是 MutablePropertySources 對象填充的過程。Environment 的靜态屬性和存儲容器都是在AbstractEnvironment 中定義的,ConfigurableWebEnvironment 接口提供的 getPropertySources() 方法可以擷取到傳回的 MutablePropertySources 執行個體,然後添加額外的 PropertySource。實際上,Environment 的存儲容器就是 PropertySource 的子類集合,而 AbstractEnvironment 中使用的執行個體就是 MutablePropertySources。

那麼到這裡相比 Environment 與配置的關系就非常清楚了,一句話概括就是:Environment 是所有配置的管理器,是 Spring 對提供配置的統一接口。前面提到 Environment 管理了所有 Spring 的環境配置,這些配置最終是以 MutablePropertySources 對象的形态存在 Environment 中。下圖為 MutablePropertySources 類的繼承體系:

SpringBoot系列-配置解析

下面繼續來看 PropertySources。

PropertySource & PropertySources

從名字就能直覺看出,PropertySources 是持有一個或者多個 PropertySource 的類。PropertySources 提供了一組基本管理 PropertySource 的方法。

PropertySource

下面看下 PropertySource 的源碼:

public abstract class PropertySource<T> {
    protected final Log logger = LogFactory.getLog(getClass());
    // 屬性名
	protected final String name;
    // 屬性源
	protected final T source;
    // 根據指定 name 和 source 建構
	public PropertySource(String name, T source) {
		Assert.hasText(name, "Property source name must contain at least one character");
		Assert.notNull(source, "Property source must not be null");
		this.name = name;
		this.source = source;
	}

    // 根據指定 name 建構,source 預設為 Object 類型
	@SuppressWarnings("unchecked")
	public PropertySource(String name) {
		this(name, (T) new Object());
	}
    // 傳回目前 PropertySource 的 name
	public String getName() {
		return this.name;
	}
    // 傳回目前 PropertySource 的 source
	public T getSource() {
		return this.source;
	}

	public boolean containsProperty(String name) {
		return (getProperty(name) != null);
	}
	@Nullable
	public abstract Object getProperty(String name);
	// 傳回用于集合比較目的的 PropertySource 實作 (ComparisonPropertySource)。
	public static PropertySource<?> named(String name) {
		return new ComparisonPropertySource(name);
	}
    // 省略其他兩個内部類實作,無實際意義
}           

複制

一個 PropertySource 執行個體對應一個 name,例如 systemProperties、enviromentProperties 等。PropertySource 包括多種類型的實作,主要包括:

  • 1、AnsiPropertySource:Ansi.*,包括 AnsiStyle、AnsiColor、AnsiBackground 等
  • 2、StubPropertySource:在實際的屬性源不能在 application context 建立時立即初始化的情況下用作占位符。例如,基于 ServletContext 的屬性源必須等待,直到 ServletContext 對象對其封裝的 ApplicationContext 可用。在這種情況下,應該使用存根來儲存屬性源的預設位置/順序,然後在上下文重新整理期間替換存根。
    • ComparisonPropertySource:繼承自 StubPropertySource ,所有屬性通路方法強制抛出異常,作用就是一個不可通路屬性的空實作。
  • 3、EnumerablePropertySource:可枚舉的 PropertySource,在其父類的基礎上擴充了 getPropertyNames 方法
    • PropertiesPropertySource:内部的 Map 執行個體由 Properties 執行個體轉換而來
    • JsonPropertySource:内部的 Map 執行個體由 Json 執行個體轉換而來
    • SystemEnvironmentPropertySource:内部的 Map 執行個體由 system env 擷取
    • CompositePropertySource:source 為組合類型的 PropertySource 實作
    • CommandLinePropertySource:source 為指令行參數類型的 PropertySource 實作,包括兩種指令行參數和 java opts 參數兩種。
    • MapPropertySource:source 為 Map 類型的 PropertySource 實作

其他還有 ServletConfigPropertySource、ServletContextPropertySource、AnnotationsPropertySource 等,均可根據名字知曉其 source 來源。

PropertySources

PropertySources 接口比較簡單,如下所示:

public interface PropertySources extends Iterable<PropertySource<?>> {
    // 從 5.1 版本才提供的
	default Stream<PropertySource<?>> stream() {
		return StreamSupport.stream(spliterator(), false);
	}
    // check name 為 「name」 的資料源是否存在
	boolean contains(String name);
    // 根據 name」 擷取資料源
	@Nullable
	PropertySource<?> get(String name);
}           

複制

前面在分析 Enviroment 建構中,可以看到整個過程都是以填充 MutablePropertySources 為主線。MutablePropertySources 是 PropertySources 的預設實作,它允許對包含的屬性源進行操作,并提供了一個構造函數用于複制現有的 PropertySources 執行個體。此外,其内部在 addFirst 和 addLast 等方法中提到了 precedence(優先順序) ,這些将會影響 PropertyResolver 解析給定屬性時搜尋屬性源的順序。

MutablePropertySources 内部就是對 propertySourceList 的一系列管理操作(增删改成等),propertySourceList 其實就是整個配置系統最底層的存儲容器,是以就很好了解,配置解析為什麼都是在填充 MutablePropertySources 這個對象了。

// 配置最終都被塞到這裡了
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();           

複制

最後我們再來看下,Spring 中 Environment 屬性是如何被通路的。

Environment 屬性通路

單從 Environment 代碼來看,其内部并沒有提供通路屬性的方法,這些通路屬性的方法都由其父類接口 PropertyResolver 提供。

public interface PropertyResolver {
    // 判斷屬性是否存在
	boolean containsProperty(String key);
    // 擷取屬性
	@Nullable
	String getProperty(String key);
    // 擷取屬性,如果沒有則提供預設值
	String getProperty(String key, String defaultValue);
	@Nullable
	<T> T getProperty(String key, Class<T> targetType);
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);
    // 擷取 Required 屬性
	String getRequiredProperty(String key) throws IllegalStateException;
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
    // 解析占位符
	String resolvePlaceholders(String text);
    // 解析 Required占位符
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}           

複制

Environment 中提供預設通路屬性的對象實作是 PropertySourcesPropertyResolver,其定義在 AbstractEnvironment 這個抽象類中:

private final ConfigurablePropertyResolver propertyResolver =
			new PropertySourcesPropertyResolver(this.propertySources);           

複制

那文章最後就來看下 PropertySourcesPropertyResolver 是如何通路配置屬性的吧。

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
        // 周遊所有的 PropertySource
        for (PropertySource<?> propertySource : this.propertySources) {
            // 省略日志
            // 從 propertySource 中根據指定的 key 擷取值
            Object value = propertySource.getProperty(key);
            // 如果值不為空->選用第一個不為 null 的比對 key 的屬性值
            if (value != null) {
                // 解析占位符替換, 如${server.port},底層委托到 PropertyPlaceholderHelper 完成
                if (resolveNestedPlaceholders && value instanceof String) {
                    value = resolveNestedPlaceholders((String) value);
                }
                logKeyFound(key, propertySource, value);
                // 進行一次類型轉換,具體由 DefaultConversionService 處理
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }
    // 省略日志 ...
    // 沒有的話就傳回 null
    return null;
}           

複制

這裡有一點需要注意,就是如果出現多個 PropertySource 中存在同名的 key,則隻會傳回第一個 PropertySource 對應 key 的屬性值。在實際的業務開發中,如果需要自定義一些環境屬性,最好要對各個 PropertySource 的順序有足夠的掌握。

小結

整體看來,Spring 中對于配置的管理還是比較簡單的,從 Environment 到 PropertySource 整個過程沒有那麼繞,就是單純的把來自各個地方的配置統一塞到 MutablePropertySources 中,對外又通過 Environment 接口對外提供接口通路。

▐ 文章來源:磊叔,轉載請事先告知,謝謝!