"配置一切" 或 "12-Factor 應用風格的 Spring 配置"

工程 | Josh Long | 2015年1月13日 | ...

在開始之前,我們先來建立一些詞彙。在 Spring 中,當我們談論 配置 時,我們 通常 指的是 Spring 框架的各種 ApplicationContext 實現的輸入,這些輸入幫助容器理解你想要完成的任務。這可能是一個要被 ClassPathXmlApplicationContext 讀取的 XML 檔案,或者是一些帶有特定註解的 Java 類,它們將被 AnnotationConfigApplicationContext 讀取。

另一種 配置 型別,正如 12-Factor 應用宣言中很好地描述的那樣,是指應用程式中可能在不同部署(暫存、生產、開發環境等)之間發生變化的所有內容,例如服務憑證和主機名。

第二種配置型別,它應該存在於已部署應用程式的外部,自 PropertyPlaceholderConfigurer 類引入以來,Spring 就一直很好地支援它。Spring 對這種配置型別的支援已經走了很長一段路,在本篇部落格中,我們將回顧這一進展。

PropertyPlaceholderConfigurer

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

# Database Credentials
configuration.projectName = Spring Framework

Environment 抽象

這個解決方案早於 Spring Framework 3.0 中引入 Java 配置。Spring 3 使使用 @Value 註解將配置值注入 Java 元件配置變得容易,如下所示:

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

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

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 分組指定標籤(profile)。使用 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 或 profiles 決定的。

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

Bootiful 配置

Spring Boot 大大改善了這些情況。Spring Boot 預設會讀取 src/main/resources/application.properties 中的屬性。如果 profile 已啟用,它還會根據 profile 名稱自動讀取配置檔案,例如 src/main/resources/application-foo.properties,其中 foo 是當前的 profile。如果 Snake YML 庫 在類路徑中,那麼它也會自動載入 YML 檔案。是的,再讀一遍。YML 非常好,非常值得嘗試!這是一個 YML 檔案示例:

configuration:
	projectName : Spring Boot
	someOtherKey : Some Other Value

Spring Boot 也使得在常見情況下獲得正確的結果更加簡單。它使 java 程序的 -D 引數和環境變數可作為屬性使用。它甚至對它們進行標準化,因此環境變數 $CONFIGURATION_PROJECTNAME 或形式為 -Dconfiguration.projectname-D 引數都可以透過鍵 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 機制來允許使用者覆蓋系統的一部分。例如,你可以透過向基於 Spring Boot 的 Web 應用程式新增 org.springframework.boot:spring-boot-starter-actuator 依賴項,然後訪問 http://127.0.0.1:8080/configprops 來檢視可用於更改事物的屬性鍵。這將為你提供基於執行時類路徑上存在的型別的支援的配置屬性列表。隨著你新增更多 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 檔案中找到的一個東西是作為屬性指定的服務的 ID,spring.application.name。這是我們的配置客戶端的 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 配置支援包括對安全和加密的一流支援。我將讓你自己去探索最後一部分,但它相當簡單,相當於配置一個有效的金鑰。

下一步

我們在這裡涵蓋了很多內容!有了所有這些,就可以很容易地打包一個工件,然後將該工件從一個環境移動到另一個環境,而無需更改工件本身。如果你今天開始開發一個應用程式,我建議從 Spring Boot 和 Spring Cloud 開始,特別是現在我們已經看到了它預設帶來的所有好處。不要忘記 檢視所有這些示例背後的程式碼

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

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

瞭解更多

即將舉行的活動

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

檢視所有