“配置萬物” 或 “使用 Spring 實現 12-Factor App 風格的配置”

工程 | Josh Long | January 13, 2015 | ...

在開始之前,讓我們確定一些術語。當我們在 Spring 中談論 配置 時,我們通常指的是 Spring 框架各種 ApplicationContext 實現的輸入,這些輸入幫助容器理解你想要完成的事情。這可能是一個要提供給 ClassPathXmlApplicationContext 的 XML 檔案,或者是一些以特定方式註解的 Java 類,要提供給 AnnotationConfigApplicationContext

另一種型別的 配置,如 12-Factor 應用宣言中所描述的,是應用的任何在不同部署環境(staging, production, developer environments 等)之間可能發生變化的部分,例如服務憑證和主機名。

自從引入 PropertyPlaceholderConfigurer 類以來,Spring 就很好地支援了第二種型別的配置,即應該存在於已部署應用外部的配置。從那時起,Spring 對這類配置的支援已經有了很大的進步,在這篇部落格中我們將回顧這一演變。

PropertyPlaceholderConfigurer

Spring 自 2003 年起就提供了 PropertyPlaceholderConfigurer。Spring 2.5 引入了 XML 名稱空間支援,並隨之帶來了屬性佔位符解析的 XML 名稱空間支援。例如,<context:property-placeholder location = "simple.properties"/> 允許我們用(外部)屬性檔案中鍵對應的值來替換 XML 配置中 bean 定義的字面值(在本例中是 simple.properties,它可以位於 classpath 中或應用外部)。這個屬性檔案可能看起來像這樣

# Database Credentials
configuration.projectName = Spring Framework

Environment 抽象

這個解決方案早於 Java 配置在 Spring Framework 3.0 中的引入。Spring 3 使使用 @Value 註解輕鬆地將配置值注入到 Java 元件配置中,像這樣

@Value("${configuration.projectName}") 
private String projectName; 

Spring 3.1 引入了 Environment 抽象。它在執行中的應用程式與其執行環境之間提供了一點執行時間接性。Environment 充當鍵值對的對映。你可以透過貢獻一個物件來配置從何處讀取這些值。在任何你想要的地方注入一個型別為 Environment 的物件,並向它提問。預設情況下,Spring 會載入系統環境鍵值對,例如 line.separator。你可以使用 @PropertySource 註解告訴 Spring 載入特定檔案中的配置鍵。

package env;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.context.support.*;
import org.springframework.core.env.Environment;

@Configuration
@ComponentScan
@PropertySource("file:/path/to/simple.properties")
public class Application {

	@Bean
	static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
		return new PropertySourcesPlaceholderConfigurer();
	}

	@Value("${configuration.projectName}")
	void setProjectName(String projectName) {
		System.out.println("setting project name: " + projectName);
	}

	@Autowired
	void setEnvironment(Environment env) {
		System.out.println("setting environment: " + 
                      env.getProperty("configuration.projectName"));
	}

	public static void main(String args[]) throws Throwable {
		new AnnotationConfigApplicationContext(Application.class);
	}
}

此示例載入 simple.properties 檔案中的值,然後使用 @Value 註解注入一個值 configuration.projectName,接著從 Spring 的 Environment 抽象中再次讀取該值。為了能夠使用 @Value 註解注入值,我們需要註冊一個 PropertySourcesPlaceholderConfigurer。在本例中,輸出為 Spring Framework

Environment 還帶來了profiles 的概念。它允許你為 bean 分組賦予標籤(profiles)。使用 profiles 來描述在不同環境之間變化的 bean 和 bean 圖。你可以一次啟用一個或多個 profile。沒有分配 profile 的 bean 總是會被啟用。帶有 default profile 的 bean 僅在沒有其他 profile 處於活動狀態時啟用。

Profiles 允許你描述需要在不同環境中以不同方式建立的 bean 集。例如,你可以在本地 dev profile 中使用嵌入式 H2 javax.sql.DataSource,而在 Cloud Foundry 中啟用 prod profile 時,切換到透過 JNDI 查詢或讀取環境變數屬性解析的 PostgreSQL 的 javax.sql.DataSource。在兩種情況下,你的程式碼都能正常工作:你獲得一個 javax.sql.DataSource,但決定使用哪個特定例項的決策由活動的 profile 或 profile 集合決定。

你應該謹慎使用此功能。理想情況下,不同環境之間的物件圖應該保持相當固定。

Bootiful 配置

Spring Boot 顯著地改進了配置方式。Spring Boot 預設會讀取 src/main/resources/application.properties 中的屬性。如果某個 profile 處於活動狀態,它還會根據 profile 名稱自動讀取配置檔案,例如 src/main/resources/application-foo.properties,其中 foo 是當前 profile。如果 classpath 中存在 Snake YML 庫,它還會自動載入 YML 檔案。是的,再讀一遍這部分。YML 太棒了,值得一試!這是一個 YML 檔案示例

configuration:
	projectName : Spring Boot
	someOtherKey : Some Other Value

Spring Boot 還極大地簡化了常見情況下的正確結果。它將 java 程序的 -D 引數和環境變數作為屬性提供。它甚至會規範化它們,因此環境變數 $CONFIGURATION_PROJECTNAME-D 形式的引數 -Dconfiguration.projectname 都可以透過鍵 configuration.projectName 進行訪問。

配置值是字串,如果你的配置值很多,確保這些鍵本身不會成為程式碼中的“魔術字串”可能會很麻煩。Spring Boot 引入了 @ConfigurationProperties 元件型別。使用 @ConfigurationProperties 註解一個 POJO 並指定一個字首,Spring 將嘗試將所有以字首開頭的屬性對映到 POJO 的屬性。在下面的示例中,configuration.projectName 的值將被對映到該 POJO 的一個例項,所有程式碼隨後都可以注入並解引用該例項來讀取(型別安全的)值。透過這種方式,你只需在一個地方進行鍵值對映。

在下面的示例中,屬性將自動從 src/main/resources/application.yml 中解析。

package boot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// reads a value from src/main/resources/application.properties first
// but would also read:
//  java -Dconfiguration.projectName=..
//  export CONFIGURATION_PROJECTNAME=..

@SpringBootApplication
public class Application {

	@Autowired
	void setConfigurationProjectProperties(ConfigurationProjectProperties cp) {
		System.out.println("configurationProjectProperties.projectName = " + cp.getProjectName());
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class);
	}
}

@Component
@ConfigurationProperties("configuration")
class ConfigurationProjectProperties {

	private String projectName;

	public String getProjectName() {
		return projectName;
	}

	public void setProjectName(String projectName) {
		this.projectName = projectName;
	}
}

Spring Boot 大量使用了 @ConfigurationProps 機制,允許使用者覆蓋系統的一些部分。你可以透過將 org.springframework.boot:spring-boot-starter-actuator 依賴項新增到基於 Spring Boot 的 web 應用程式中,然後訪問 http://127.0.0.1:8080/configprops 來檢視哪些屬性鍵可以用於更改事物。這將根據執行時 classpath 中存在的型別為你提供支援的配置屬性列表。隨著你新增更多 Spring Boot 型別,你將看到更多屬性。

使用 Spring Cloud 配置支援實現集中式、日誌記錄的配置

到目前為止一切順利,但目前的方法存在一些不足之處

  • 應用配置的更改需要重啟
  • 沒有可追溯性:我們如何確定哪些更改已引入生產環境,並在必要時進行回滾?
  • 配置是分散的,不清楚應該到哪裡去更改什麼。
  • 有時出於安全考慮,配置值應該被加密和解密。對此沒有開箱即用的支援。

Spring Cloud,它基於 Spring Boot 構建,並集成了各種用於微服務工作的工具和庫,包括 Netflix OSS 棧,提供了配置伺服器和該配置伺服器的客戶端。這些支援結合起來解決了最後三個問題。

讓我們來看一個簡單的例子。首先,我們將設定一個配置伺服器。配置伺服器是在一組基於 Spring Cloud 的應用或微服務之間共享的。你需要將它執行起來,部署在某個地方,一次就好。然後,所有其他服務只需要知道在哪裡可以找到配置服務。配置服務充當配置鍵值對的代理,它從線上或本地磁碟的 Git 倉庫中讀取這些鍵值對。

package cloud.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

如果你管理得當,那麼你的任何服務中唯一的配置應該是告訴配置服務去哪裡找到 Git 倉庫的配置,以及告訴其他客戶端服務去哪裡找到配置服務的配置。

這是配置服務的配置,src/main/resources/application.yml

server:
	port: 8888

spring:
	cloud:
		config:
			server:
				git :
					uri: https://github.com/joshlong/microservices-lab-configuration

這告訴 Spring Cloud 配置服務在我的 GitHub 帳戶上的 Git 倉庫中查詢單個客戶端服務的配置檔案。當然,URI 也可以很輕鬆地是我的本地檔案系統上的一個 Git 倉庫。用於 URI 的值也可以是一個屬性引用,形式如 ${SOME_URI},它可能引用一個名為 SOME_URI 的環境變數。

執行應用程式,你將可以透過將瀏覽器指向 https://:8888/SERVICE/master 來驗證你的配置服務是否正常工作,其中 SERVICE 是從你的客戶端服務 boostrap.yml 中獲取的 ID。基於 Spring Cloud 的服務會查詢一個名為 src/main/resources/bootstrap.(properties,yml) 的檔案,它期望在該檔案中找到用於——你猜對了!——引導服務的資訊。它期望在 bootstrap.yml 檔案中找到的一項內容是指定為屬性 spring.application.name 的服務 ID。這是我們的配置客戶端的 bootstrap.yml

spring:
	application:
		name: config-client
		cloud:
			config:
				uri: https://:8888

當 Spring Cloud 微服務執行時,它會看到其 spring.application.nameconfig-client。它將聯絡配置服務(我們告訴 Spring Cloud 它執行在 http://localhosst:8080,儘管這也可能是一個環境變數)並向其請求任何配置。配置服務返回 JSON,其中包含 application.(properties,yml) 檔案中的所有配置值以及 config-client.(yml,properties) 中的任何服務特定配置。它還會載入給定服務特定 profile 的任何配置,例如 config-client-dev.properties

這一切都是自動發生的。在下面的示例中,配置值是從配置服務中讀取的。

package cloud.client;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Autowired
	void setEnvironment(Environment e) {
		System.out.println(e.getProperty("configuration.projectName"));
	}
}

@RestController
@RefreshScope
class ProjectNameRestController {

	@Value("${configuration.projectName}")
	private String projectName;

	@RequestMapping("/project-name")
	String projectName() {
		return this.projectName;
	}
}

ProjectNameRestController@RefreshScope 註解,這是一個自定義的 Spring Cloud scope,允許任何 bean 原地重新建立自己(並從配置服務重新讀取配置值)。有幾種方法可以觸發重新整理:向 http://127.0.0.1:8080/refresh 傳送 POST 請求(例如:curl -d{} http://127.0.0.1:8080/refresh),使用自動暴露的 JMX 重新整理端點,或使用 Spring Cloud Bus。

Spring Cloud Bus 透過基於 RabbitMQ 的匯流排連線所有服務。這尤其強大。你可以透過向訊息匯流排傳送一條訊息來告訴一個(或數千個!)微服務重新整理自身。這避免了停機,並且比系統地重啟單個服務或節點要友善得多。

要看到這一切的實際效果,請執行配置客戶端和配置伺服器,確保將配置伺服器指向你可以控制並進行更改的 Git 倉庫。訪問 REST 端點並確認你看到 Spring Cloud。然後更改 Git 中的配置檔案,並至少 git commit 這些更改。然後觸發配置客戶端的重新整理,並再次訪問 REST 端點。你應該會看到更新的值反映出來!

Spring Cloud 配置支援包含了對安全性和加密的一流支援。我將留給你自己去探索這最後一部分,但這相當簡單,只需要配置一個有效的金鑰。

下一步

我們在這裡講了很多內容!有了這一切,應該可以輕鬆打包一個 artifact,然後將該 artifact 從一個環境移動到另一個環境,而無需更改 artifact 本身。如果你今天打算開始一個應用,我推薦從 Spring Boot 和 Spring Cloud 開始,尤其是我們已經瞭解了它預設帶來的所有好處。別忘了檢視所有這些示例背後的程式碼

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助你快速前進。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一個簡單的訂閱。

瞭解更多

即將舉辦的活動

檢視 Spring 社群所有即將舉辦的活動。

檢視全部