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

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

在開始之前,讓我們先建立一些詞彙。當我們在 Spring 中談論配置時,我們通常指的是 Spring Framework 各種 ApplicationContext 實現的輸入,這些輸入有助於容器理解您想要完成的任務。這可能是一個要饋送到 ClassPathXmlApplicationContext 的 XML 檔案,或者是以特定方式註釋的 Java 類別,要饋送到 AnnotationConfigApplicationContext

另一種配置,正如 12-Factor 應用程式宣言中描述的那樣,是應用程式中可能因部署(暫存、生產、開發人員環境等)而異的任何內容,例如服務憑證和主機名稱。

自從引入 PropertyPlaceholderConfigurer 類別以來,Spring 對第二種類型的配置(應該存在於已部署應用程式外部)提供了良好的支持。 Spring 對該類型配置的支持自那時以來已經取得了長足的進步,在本部落格中,我們將了解其發展歷程。

PropertyPlaceholderConfigurer

自 2003 年以來,Spring 一直提供 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 Framework 本身。 Spring 3 使使用 @Value 註釋將配置值注入到 Java 元件配置中變得容易,如下所示

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

Spring 3.1 引入了 Environment 抽象。它在運行中的應用程式和運行環境之間提供了一些運行時的間接性。 Environment 充當鍵和值的映射。您可以透過貢獻 a 來配置從何處讀取這些值。在您想要的任何地方注入 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 的分組。使用設定檔來描述在不同環境之間變更的 bean 和 bean 圖。您可以一次啟動一個或多個設定檔。未分配設定檔的 bean 始終處於啟動狀態。只有在沒有其他設定檔處於啟動狀態時,才會啟動具有設定檔 default 的 bean。

設定檔允許您描述需要在一個環境與另一個環境中以不同方式建立的 bean 集合。例如,您可能會在本地 dev 設定檔中使用嵌入式 H2 javax.sql.DataSource,然後切換到 PostgreSQL 的 javax.sql.DataSource,該數據源透過 JNDI 查找或透過從 Cloud Foundry 中環境變數讀取屬性來解析,當 prod 設定檔處於啟動狀態時。在這兩種情況下,您的程式碼都能正常運作:您會獲得一個 javax.sql.DataSource,但關於使用哪個專用實例的決定由啟動的設定檔或設定檔決定。

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

Bootiful 配置

Spring Boot 大幅改善了情況。 Spring Boot 預設會讀取 src/main/resources/application.properties 中的屬性。 如果設定檔處於啟動狀態,它也會自動讀取基於設定檔名稱的設定檔,例如 src/main/resources/application-foo.properties,其中 foo 是目前的設定檔。如果 Snake YML 程式庫 位於類別路徑上,那麼它也會自動載入 YML 檔案。是啊,再讀一遍。 YML 非常棒,而且非常值得一試!以下是一個 YML 檔案範例

configuration:
	projectName : Spring Boot
	someOtherKey : Some Other Value

Spring Boot 也讓在常見情況下獲得正確結果變得更加簡單。它使 -D 引數傳遞到 java 流程和環境變數可用作屬性。它甚至會對它們進行正規化,因此 $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 機制,讓使用者可以覆寫系統的位元。您可以透過將 org.springframework.boot:spring-boot-starter-actuator 依賴項新增到基於 Spring Boot 的 Web 應用程式,然後造訪 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://127.0.0.1:8888/SERVICE/master 來驗證您的配置服務是否正常運作,其中 SERVICE 是從您的用戶端服務的 boostrap.yml 中取得的 ID。基於 Spring Cloud 的服務會尋找名為 src/main/resources/bootstrap.(properties,yml) 的檔案,它預期會找到該檔案來引導 (bootstrap) 服務。它預期在 bootstrap.yml 檔案中找到的一件事是以屬性 spring.application.name 指定的服務 ID。以下是我們配置用戶端的 bootstrap.yml

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

當 Spring Cloud 微服務執行時,它會看到它的 spring.application.nameconfig-client。它會聯絡配置服務(我們已經告訴 Spring Cloud 該服務在 http://localhosst:8080 上執行,儘管這也可以是一個環境變數),並要求它提供任何配置。配置服務會傳回包含 application.(properties,yml) 檔案中所有配置值的 JSON,以及 config-client.(yml,properties) 中任何特定於服務的配置。它也會載入給定服務特定配置檔的任何配置,例如,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 社群中所有即將舉行的活動。

查看全部