在任何一套開發框架中, 多環境管裡 通常是重要的核心功能之一,當然在 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 {@Beanpublic 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 {@Beanpublic 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