The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

深入淺出 Spring Boot 多重設定檔管理 (Spring Profiles)

在任何一套開發框架中,多環境管理通常是重要的核心功能之一,當然在 Spring 框架中也不例外,這裡我們稱為 Spring Profiles 設定檔。這個功能說起來簡單,但實作起來卻很容易會不小心亂掉,這篇文章我打算來好好的梳理一番,把觀念搞懂,管理才不會亂掉。

Spring Boot

建立範例應用程式

  1. 使用 Spring Boot CLI 快速建立專案 (也可以用 Spring Initializr 建立)

    spring init --dependencies=web --groupId=com.duotify sbprofile1
    

    使用 Visual Studio Code 開啟該專案

    code sbprofile1
    
  2. 加入一個 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";
        }
    }
    
  3. 測試執行

    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),快速的切換應用程式配置,就這麼簡單!

其中應用程式配置包含了兩層含意:

  1. 組態配置(Configuration)

    所謂組態配置其實就是像 src/main/resources/application.properties 這種屬性定義檔。

  2. 應用程式元件組合(Components combination)

    所謂應用程式元件組合就是指應用程式中有哪些「元件」要啟用,你可以在執行時期透過簡單的參數,決定本次執行要用什麼 Profile 來啟動應用程式。

使用 Profile 來管理應用程式配置,最常見的例子,就是用在「多環境」部署上,例如你有公司內部的「測試環境」與客戶提供的「正式環境」,兩者的組態設定通常都不太一樣,但也有些一樣的地方。此時,我們就可以透過多個 Profile 來管理這些差異,抽象化之後,我們只要知道設定名稱(Profile Name)就可以切換不同環境。

如何使用應用程式屬性(Application Properties)

在理解如何管理多個設定檔之前,應該要先瞭解應用程式屬性(Application Properties)應該怎麼用。

體驗的步驟如下:

  1. 編輯 src/main/resources/application.properties 屬性檔

    加入一個 my.profile 屬性值

    my.profile=dev
    
  2. 調整 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;
        }
    }
    
  3. 測試執行

    mvn clean spring-boot:run
    

    使用 cURL 測試

    $ curl localhost:8080
    Hello World: dev
    

如何透過 Maven、命令列參數、環境變數、.env 傳入屬性值

有些時候我們想透過 Maven 在應用程式的「編譯時期」加入屬性值,此時就會需要一個方法將 Maven 的 pom.xml 定義的屬性傳入到 src/main/resources/application.properties 屬性檔中。

體驗的步驟如下:

  1. 編輯 src/main/resources/application.properties 屬性檔

    加入一個 my.profile 屬性值

    my.profile=@my.profile@
    
  2. 調整 pom.xml 檔,在 <properties> 加入一個 <my.profile> 屬性

    <my.profile>dev2</my.profile>
    
  3. 測試執行

    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 設定檔。

以下是體驗步驟:

  1. 編輯 src/main/resources/application.properties 屬性檔

    加入一個 spring.profiles.active 屬性值

    spring.profiles.active=@spring.profiles.active@
    

    這裡左邊的 spring.profiles.active 是 Spring 框架會使用的屬性名稱,而右邊的 @spring.profiles.active@ 則是一個可以從外部傳入的屬性。

  2. 調整 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 屬性 / 環境變數 / 命令列參數)
    
  3. 修改 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;
        }
    }
    
  4. 測試執行

    請記得我們現在有兩個設定檔,分別是 defaultdev8 這兩個。當我們用 mvn spring-boot:run 啟動應用程式時,就可以用 -P 外加一個 ProfileName 就可以啟用設定檔。

    mvn clean spring-boot:run -Pdev8
    

    注意: 這裡的 -PP 必須用字母大寫,而且後面接上的名稱是 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 才是正確的命名規則。

接著我們就來體驗一下多設定檔的套用情況:

  1. 我們再加入一個 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>
    
  2. 除了 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 (空白內容)

     
  3. 修改 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;
        }
    }
    
  4. 測試執行

    請記得我們現在有 3 個設定檔,分別是 default, dev8dev9 這三個。

    先嘗試不指定 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 下載入,以下是使用範例:

  1. 建立一個 UserService 類別

    package com.duotify.sbprofile1.services;
    
    public class UserService {
        public UserService(String name) {
            this.name = name;
        }
        private String name;
        public String getName() {
            return name;
        }
    }
    
  2. 建立一個 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 執行。

  3. 建立一個 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 執行。

  4. 修改 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();
        }
    }
    
  5. 修改 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>
    
  6. 測試執行

    請記得我們現在有 5 個設定檔,分別是 default, dev8, dev9, devprod 這五個。

    嘗試指定 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 說明。

相關連結

留言評論