在任何一套開發框架中,多環境管理通常是重要的核心功能之一,當然在 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 執行喔! 👍
使用 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
-
直接從 .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>
相依套件,例如載入相同套件的不同版本(測試新舊版本),或是相同介面但不同套件(不同資料庫驅動程式)之類的,這點真的很讚! 👍
-
以下是相同套件不同版本的設定範例:
<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
另一種實際的例子可以參考 7. Example: Separate Data Source Configurations Using Profiles 提供的範例。
由於 Maven Profiles 博大精深,還有更多用法可以參考 Guide to Maven Profiles 說明。
相關連結