在任何一套開發框架中,?多環境管裡?通常是重要的核心功能之一,當然在 Spring 框架中也不例外,這裡我們稱為?Spring Profiles?設定檔。這個功能說起來簡單,但實作起來卻很容易會不小心亂掉,這篇文章我打算來好好的梳理一番,把觀念搞懂,管裡才不會亂掉。
建立範例應用程式
-
使用 Spring Boot CLI 快速建立專案 (也可以用?Spring Initializr?建立) spring init --dependencies=web --groupId=com.duotify sbprofile1 使用 Visual Studio Code 開啟該專案 code sbprofile1 -
加入一個?HomeController ?控制器 檔名路徑:?src/main/java/com/duotify/sbprofile1/controllers/HomeController.java package com.duotify.sbprofile1.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@GetMapping("/")
public String home() {
return "Hello World";
}
} -
測試執行 mvn clean spring-boot:run 補充說明: 你可以在?pom.xml ?的?<build> ?底下新增一個?<defaultGoal>spring-boot:run</defaultGoal> ?設定,未來就只要打?mvn ?就會自動啟動 Spring Boot 執行喔! :+1: 使用 cURL 測試 $ curl localhost:8080
Hello World
理解設定檔(Profiles)的真正含意
由於 Spring 框架有一大堆抽象概念,不好好花點時間研究,就會有很多魔鬼般的細節無法理解。本文所提到的 Spring Profiles 原本是一個很簡單的概念,但是在寫 Spring Boot 的時候卻有非常多種變化,多到可以讓你腦袋打結那種。
我們先從最簡單、最抽象的概念開始講起。
所謂?Profile?(設定檔) 通常有個?名字?(Profile Name),這個?名字?代表一組?應用程式配置?。你可以透過一個簡單的?設定名稱?(Profile Name),快速的切換?應用程式配置?,就這麼簡單!
其中?應用程式配置?包含了兩層含意:
-
組態配置(Configuration) 所謂?組態配置?其實就是像?src/main/resources/application.properties ?這種屬性定義檔。 -
應用程式元件組合(Components combination) 所謂?應用程式元件組合?就是指應用程式中有哪些「元件」要啟用,你可以在?執行時期?透過簡單的參數,決定本次執行要用什麼?Profile?來啟動應用程式。
使用?Profile?來管裡?應用程式配置?,最常見的例子,就是用在「多環境」部署上,例如你有公司內部的「測試環境」與客戶提供的「正式環境」,兩者的組態設定通常都不太一樣,但也有些一樣的地方。此時,我們就可以透過多個?Profile?來管裡這些差異,抽象化之後,我們只要知道?設定名稱?(Profile Name)就可以切換不同環境。
如何使用應用程式屬性(Application Properties)
在理解如何?管裡多個設定檔?之前,應該要先瞭解?應用程式屬性?(Application Properties)應該怎麼用。
體驗的步驟如下:
-
編輯?src/main/resources/application.properties ?屬性檔 加入一個?my.profile ?屬性值 my.profile=dev -
調整?HomeController ?控制器,加入一個私有欄位(Private Field),並透過?@Value ?標注來注入一個?my.profile ?屬性值 檔名路徑:?src/main/java/com/duotify/sbprofile1/controllers/HomeController.java package com.duotify.sbprofile1.controllers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@Value("${my.profile}")
private String myProfile;
@GetMapping("/")
public String home() {
return "Hello World: " + this.myProfile;
}
} -
測試執行 mvn clean spring-boot:run 使用 cURL 測試 $ curl localhost:8080
Hello World: dev
如何透過 Maven、命令列參數、環境變數、.env 傳入屬性值
有些時候我們想透過 Maven 在應用程式的「編譯時期」加入屬性值,此時就會需要一個方法將 Maven 的?pom.xml ?定義的屬性傳入到?src/main/resources/application.properties ?屬性檔中。
體驗的步驟如下:
-
編輯?src/main/resources/application.properties ?屬性檔 加入一個?my.profile ?屬性值 my.profile=@my.profile@ -
調整?pom.xml ?檔,在?<properties> ?加入一個?<my.profile> ?屬性 <my.profile>dev2</my.profile> -
測試執行 mvn clean spring-boot:run 使用 cURL 測試 $ curl localhost:8080
Hello World: dev2
我們在?application.properties ?屬性檔中的?@my.profile@ ?語法非常特別,他會宣告你將從?外部?讀取屬性值,如果 Maven 有定義屬性的話,預設會在編譯專案時加入成為預設值。不過,這種語法還有一個優點,那就是可以讓你在?執行時期?才透過各種方法?賦值?(Assign Value)。例如:
-
直接從?命令列參數?傳入參數 mvn clean spring-boot:run -Dmy.profile=dev3 $ curl localhost:8080
Hello World: dev3 -
直接從?環境變數?傳入屬性值 以下是 Bash 設定環境變數的語法: my_profile=dev4 mvn clean spring-boot:run $ curl localhost:8080
Hello World: dev4 環境變數遇到屬性名稱有小數點(?. ?)的時候,記得轉成底線(?_ ?)才可以。 -
先封裝成 JAR 檔,透過?java -jar ?執行時也可以透過?命令列參數?傳入參數 mvn clean package java -Dmy.profile=dev5 -jar target/sbprofile1-0.0.1-SNAPSHOT.jar 這個?-Dmy.profile=dev5 ?參數會傳入 JVM 當成系統參數使用。 $ curl localhost:8080
Hello World: dev5 請記得?-Dmy.profile=dev5 ?一定要設定在?-jar ?前面! -
先封裝成 JAR 檔,透過?java -jar ?執行時也可以透過?環境變數?傳入參數 mvn clean package my_profile=dev6 java -jar target/sbprofile1-0.0.1-SNAPSHOT.jar $ curl localhost:8080
Hello World: dev6 請記得?-Dmy.profile=dev5 ?一定要設定在?-jar ?前面! -
直接從?.env ?檔案定義的?環境變數?傳入參數 先在專案根目錄加入一個?.env ?檔,內容如下: my_profile=dev7 建立一個?.vscode/launch.json ?啟動設定檔 (VSCode) {
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Launch DemoApplication",
"request": "launch",
"mainClass": "com.duotify.sbprofile1.DemoApplication",
"projectName": "sbprofile1",
"envFile": "${workspaceFolder}/.env"
}
]
} 這裡的重點在於?envFile ?設定。 按下?F5 ?啟動專案,就可以讀到設定值了! $ curl localhost:8080
Hello World: dev7
理解 Spring Profiles 設定檔的使用方式
在瞭解了 Properties 檔的使用與設定方式後,終於可以進入本文的重點內容,那就是如何定義 Spring Profiles 設定檔。
以下是體驗步驟:
-
編輯?src/main/resources/application.properties ?屬性檔 加入一個?spring.profiles.active ?屬性值 spring.profiles.active=@spring.profiles.active@ 這裡左邊的?spring.profiles.active ?是 Spring 框架會使用的屬性名稱,而右邊的?@spring.profiles.active@ ?則是一個可以從?外部?傳入的屬性。 -
調整 Maven 的?pom.xml ?設定檔,加入?<profiles> ?區段設定 <profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>default</spring.profiles.active>
</properties>
</profile>
<profile>
<id>dev8</id>
<properties>
<spring.profiles.active>dev8</spring.profiles.active>
</properties>
</profile>
</profiles> 這段設定比較特別的地方在於,我們定義了?2 ?份 Profiles,一個是我們的?default ?設定檔,另一個則是?dev8 ?設定檔。然而不同的?設定檔?各有定義一個特別的?spring.profiles.active ?屬性 (你也可以定義多個屬性),這個屬性專門是用來給 Spring 應用程式參考?目前啟用的設定檔?是誰。 請記得: 你在?pom.xml ?定義的屬性(?<properties> ?)並不會直接給 Java 程式參考,他們之間的關係是: Java 原始檔 <-- 應用程式屬性檔(.properties/.yml) <-- 外部傳入屬性 (Maven 屬性 / 環境變數 / 命令列參數) -
修改?HomeController ?的?@Value ?標注,改注入?spring.profiles.active ?屬性 package com.duotify.sbprofile1.controllers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@Value("${spring.profiles.active}")
private String myProfile;
@GetMapping("/")
public String home() {
return "Hello World: " + this.myProfile;
}
} -
測試執行 請記得我們現在有兩個設定檔,分別是?default ?與?dev8 ?這兩個。當我們用?mvn spring-boot:run ?啟動應用程式時,就可以用?-P ?外加一個?ProfileName ?就可以啟用設定檔。 mvn clean spring-boot:run -Pdev8 注意: 這裡的?-P ?的?P ?必須用字母大寫,而且後面接上的名稱是?pom.xml ?當中的?<id> ?元素值! 使用 cURL 測試 $ curl localhost:8080
Hello World: dev8 如果嘗試傳入一個?不存在的?dev9 ?設定檔名稱,將會得到預設的設定檔: mvn clean spring-boot:run -Pdev9 $ curl localhost:8080
Hello World: default
注意: 同時要啟用兩個 Profiles 是可以的,透過?-P ?搭配逗號分隔即可。例如你可以用以下命令來測試啟用的 Profile 名稱:?mvn help:active-profiles -Pdev,prod
透過 Spring Profiles 切換不同的應用程式屬性檔
使用 Spring 框架的 Profiles 功能,有另外一個好處,那就是你可以不用把屬性都設定在 Maven 的?pom.xml ?檔裡面,而是可以透過一種特殊的命名習慣,將應用程式屬性設定在不同的?.properties ?檔案中。以下檔名規格請見註解說明:
# 這是預設的應用程式屬性檔,無論啟用哪一個設定,都會載入這個檔案中的屬性
application.properties
# 這是特定設定檔會套用的應用程式屬性檔,只有啟用的屬性檔會載入檔案中的屬性
application-{ProfileName}.properties
請注意: 在?application ?檔名後面要接上?- ?(dash) 符號,然後才是接上你的?ProfileName ?才是正確的命名規則。
接著我們就來體驗一下多?設定檔?的套用情況:
-
我們再加入一個?dev9 ?設定檔到?pom.xml ?檔中 <profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>default</spring.profiles.active>
</properties>
</profile>
<profile>
<id>dev8</id>
<properties>
<spring.profiles.active>dev8</spring.profiles.active>
</properties>
</profile>
<profile>
<id>dev9</id>
<properties>
<spring.profiles.active>dev9</spring.profiles.active>
</properties>
</profile>
</profiles> -
除了?application.properties ?之外,我們額外建立兩個應用程式屬性檔 檔案 1:?src/main/resources/application.properties ?(加入一個?my.name ?屬性) my.profile=@my.profile@
spring.profiles.active=@spring.profiles.active@
my.name=Will 檔案 2:?src/main/resources/application-dev8.properties ?(加入一個?my.name ?屬性) my.name=John 檔案 3:?src/main/resources/application-dev9.properties ?(空白內容) -
修改?HomeController ?的?@Value ?標注,改注入?spring.profiles.active ?屬性 package com.duotify.sbprofile1.controllers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@Value("${spring.profiles.active}")
private String myProfile;
@Value("${my.name}")
private String myName;
@GetMapping("/")
public String home() {
return "Hello World: " + this.myName;
}
} -
測試執行 請記得我們現在有?3 ?個設定檔,分別是?default ?,?dev8 ?與?dev9 ?這三個。 先嘗試不指定 Profile 的情況 mvn clean spring-boot:run $ curl localhost:8080
Hello World: Will 再嘗試指定 Profile?dev8 ?的情況 mvn clean spring-boot:run -Pdev8 $ curl localhost:8080
Hello World: John 最後嘗試指定 Profile?dev9 ?的情況 mvn clean spring-boot:run -Pdev9 $ curl localhost:8080
Hello World: Will
透過 Spring Profiles 載入不同的相依套件
透過 Spring Profiles 設定檔的方式進行組態設定,除了可以設定「屬性」之外,還能依據不同?設定檔?(Profile)來載入不同的?<dependencies> ?相依套件,例如載入?相同套件?的?不同版本?(測試新舊版本),或是?相同介面?但?不同套件?(不同資料庫驅動程式)之類的,這點真的很讚! :+1:
-
以下是?相同套件不同版本?的設定範例: <profile>
<id>dev8</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
<properties>
<spring.profiles.active>dev8</spring.profiles.active>
</properties>
</profile> 如果想看套用不同 Profile 之後的相依套件資訊,可以執行以下命令: mvn dependency:tree -Pdev8 -
以下是?相同介面不同套件?的設定範例: <profiles>
<profile>
<id>Local</id>
<dependencies>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.3.3</version>
<classifier>jdk5</classifier>
</dependency>
</dependencies>
<properties>
<jdbc.url>jdbc:hsqldb:file:databaseName</jdbc.url>
<jdbc.username>a</jdbc.username>
<jdbc.password></jdbc.password>
<jdbc.driver>org.hsqldb.jdbcDriver</jdbc.driver>
</properties>
</profile>
<profile>
<id>MySQL</id>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
<properties>
<jdbc.url>jdbc:mysql://mysql.website.ac.uk:3306</jdbc.url>
<jdbc.username>user</jdbc.username>
<jdbc.password>1234</jdbc.password>
<jdbc.driver>com.mysql.jdbc.Driver</jdbc.driver>
</properties>
</profile>
</profiles>
透過 Spring Profiles 載入不同 Beans 元件
在 Spring 框架下,所有套用?@Component ?標注的類別全部都會被註冊成 Beans 元件,其中當然也包含套用?@Configuration ?標注的類別,因為這些標注都繼承自?@Component ?介面。
然而,你只要很簡單的在類別上額外套用?@Profile ?標注,就可以宣告 Spring 要在特定 Profile 下載入,以下是使用範例:
-
建立一個?UserService ?類別 package com.duotify.sbprofile1.services;
public class UserService {
public UserService(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
} -
建立一個?UserServiceDev ?類別 package com.duotify.sbprofile1.services;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("dev")
public class UserServiceDev {
@Bean
public UserService getUserService() {
return new UserService("Dev");
}
} 這個?UserServiceDev ?只有在啟用?dev ?設定檔時才會被 Spring 執行。 -
建立一個?UserServiceProd ?類別 package com.duotify.sbprofile1.services;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("!dev")
public class UserServiceProd {
@Bean
public UserService getUserService() {
return new UserService("Prod");
}
} 這個?UserServiceDev ?只有在啟用 non-?dev ?設定檔時才會被 Spring 執行。 -
修改?HomeController ?並透過「建構式」注入?UserService ?服務 package com.duotify.sbprofile1.controllers;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.duotify.sbprofile1.services.UserService;
@RestController
public class HomeController {
@Value("${spring.profiles.active}")
private String myProfile;
@Value("${my.name}")
private String myName;
private UserService svc;
public HomeController(UserService svc) {
this.svc = svc;
}
@GetMapping("/")
public String home() {
return "Hello World: " + this.svc.getName();
}
} -
修改?pom.xml ?再加入兩個?<profile> ?定義 <profile>
<id>dev</id>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile> -
測試執行 請記得我們現在有?5 ?個設定檔,分別是?default ?,?dev8 ?,?dev9 ?,?dev ?與?prod ?這五個。 嘗試指定 Profile?dev ?的情況 mvn clean spring-boot:run -Pdev $ curl localhost:8080
Hello World: Dev 嘗試指定 Profile?prod ?的情況 mvn clean spring-boot:run -Pprod $ curl localhost:8080
Hello World: Prod ?
|