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

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

![Spring Boot](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/ee48525cf44368c298c4-191389828-1ef33b15-2555-4266-8163-a8b8d5639904.png)

### 建立範例應用程式

1.  使用 Spring Boot CLI 快速建立專案 (也可以用 [Spring Initializr](https://start.spring.io/) 建立)
    
    ```bash
    spring init --dependencies=web --groupId=com.duotify sbprofile1
    ```
    
    使用 Visual Studio Code 開啟該專案
    
    ```bash
    code sbprofile1
    ```
    
2.  加入一個 `HomeController` 控制器
    
    檔名路徑: `src/main/java/com/duotify/sbprofile1/controllers/HomeController.java`
    
    ```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.  測試執行
    
    ```bash
    mvn clean spring-boot:run
    ```
    
    > 補充說明: 你可以在 `pom.xml` 的 `<build>` 底下新增一個 `<defaultGoal>spring-boot:run</defaultGoal>` 設定，未來就只要打 `mvn` 就會自動啟動 Spring Boot 執行喔！ 👍
    
    使用 cURL 測試
    
    ```bash
    $ 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` 屬性值
    
    ```ini
    my.profile=dev
    ```
    
2.  調整 `HomeController` 控制器，加入一個私有欄位(Private Field)，並透過 `@Value` 標注來注入一個 `my.profile` 屬性值
    
    檔名路徑: `src/main/java/com/duotify/sbprofile1/controllers/HomeController.java`
    
    ```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.  測試執行
    
    ```bash
    mvn clean spring-boot:run
    ```
    
    使用 cURL 測試
    
    ```bash
    $ 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` 屬性值
    
    ```ini
    my.profile=@my.profile@
    ```
    
2.  調整 `pom.xml` 檔，在 `<properties>` 加入一個 `<my.profile>` 屬性
    
    ```xml
    <my.profile>dev2</my.profile>
    ```
    
3.  測試執行
    
    ```bash
    mvn clean spring-boot:run
    ```
    
    使用 cURL 測試
    
    ```bash
    $ curl localhost:8080
    Hello World: dev2
    ```
    

我們在 `application.properties` 屬性檔中的 `@my.profile@` 語法非常特別，他會宣告你將從**外部**讀取屬性值，如果 Maven 有定義屬性的話，預設會在編譯專案時加入成為預設值。不過，這種語法還有一個優點，那就是可以讓你在**執行時期**才透過各種方法**賦值**(Assign Value)。例如：

-   直接從**命令列參數**傳入參數
    
    ```bash
    mvn clean spring-boot:run -Dmy.profile=dev3
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: dev3
    ```
    
-   直接從**環境變數**傳入屬性值
    
    以下是 Bash 設定環境變數的語法：
    
    ```bash
    my_profile=dev4 mvn clean spring-boot:run
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: dev4
    ```
    
    > 環境變數遇到屬性名稱有小數點(`.`)的時候，記得轉成底線(`_`)才可以。
    
-   先封裝成 JAR 檔，透過 `java -jar` 執行時也可以透過**命令列參數**傳入參數
    
    ```bash
    mvn clean package
    ```
    
    ```bash
    java -Dmy.profile=dev5 -jar target/sbprofile1-0.0.1-SNAPSHOT.jar
    ```
    
    > 這個 `-Dmy.profile=dev5` 參數會傳入 JVM 當成系統參數使用。
    
    ```bash
    $ curl localhost:8080
    Hello World: dev5
    ```
    
    > 注意: `-Dmy.profile=dev5` 一定要設定在 `-jar` 前面！
    
-   先封裝成 JAR 檔，透過 `java -jar` 執行時也可以透過**環境變數**傳入參數
    
    ```bash
    mvn clean package
    ```
    
    ```bash
    my_profile=dev6 java -jar target/sbprofile1-0.0.1-SNAPSHOT.jar
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: dev6
    ```
    
-   直接從 `.env` 檔案定義的**環境變數**傳入參數
    
    先在專案根目錄加入一個 `.env` 檔，內容如下：
    
    ```bash
    my_profile=dev7
    ```
    
    建立一個 `.vscode/launch.json` 啟動設定檔 (VSCode)
    
    ```json
    {
        "version": "0.2.0",
        "configurations": [
            {
                "type": "java",
                "name": "Launch DemoApplication",
                "request": "launch",
                "mainClass": "com.duotify.sbprofile1.DemoApplication",
                "projectName": "sbprofile1",
                "envFile": "${workspaceFolder}/.env"
            }
        ]
    }
    ```
    
    > 這裡的重點在於 `envFile` 設定。
    
    按下 `F5` 啟動專案，就可以讀到設定值了！
    
    ```bash
    $ curl localhost:8080
    Hello World: dev7
    ```
    

### 理解 Spring Profiles 設定檔的使用方式

在瞭解了 Properties 檔的使用與設定方式後，終於可以進入本文的重點內容，那就是如何定義 Spring Profiles 設定檔。

以下是體驗步驟：

1.  編輯 `src/main/resources/application.properties` 屬性檔
    
    加入一個 `spring.profiles.active` 屬性值
    
    ```ini
    spring.profiles.active=@spring.profiles.active@
    ```
    
    > 這裡左邊的 `spring.profiles.active` 是 Spring 框架會使用的屬性名稱，而右邊的 `@spring.profiles.active@` 則是一個可以從**外部**傳入的屬性。
    
2.  調整 Maven 的 `pom.xml` 設定檔，加入 `<profiles>` 區段設定
    
    ```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>
    </profiles>
    ```
    
    這段設定比較特別的地方在於，我們定義了 `2` 份 Profiles，一個是我們的 `default` 設定檔，另一個則是 `dev8` 設定檔。然而不同的**設定檔**各有定義一個特別的 `spring.profiles.active` 屬性 (你也可以定義多個屬性)，這個屬性專門是用來給 Spring 應用程式參考**目前啟用的設定檔**是誰。
    
    請記得: 你在 `pom.xml` 定義的屬性(`<properties>`)並不會直接給 Java 程式參考，他們之間的關係是：
    
    ```
    Java 原始檔 <-- 應用程式屬性檔(.properties/.yml) <-- 外部傳入屬性 (Maven 屬性 / 環境變數 / 命令列參數)
    ```
    
3.  修改 `HomeController` 的 `@Value` 標注，改注入 `spring.profiles.active` 屬性
    
    ```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("${spring.profiles.active}")
        private String myProfile;
    
        @GetMapping("/")
        public String home() {
            return "Hello World: " + this.myProfile;
        }
    }
    ```
    
4.  測試執行
    
    請記得我們現在有兩個設定檔，分別是 `default` 與 `dev8` 這兩個。當我們用 `mvn spring-boot:run` 啟動應用程式時，就可以用 `-P` 外加一個 `ProfileName` 就可以啟用設定檔。
    
    ```bash
    mvn clean spring-boot:run -Pdev8
    ```
    
    > 注意: 這裡的 `-P` 的 `P` 必須用字母大寫，而且後面接上的名稱是 `pom.xml` 當中的 `<id>` 元素值！
    
    使用 cURL 測試
    
    ```bash
    $ curl localhost:8080
    Hello World: dev8
    ```
    
    如果嘗試傳入一個**不存在的** `dev9` 設定檔名稱，將會得到預設的設定檔：
    
    ```bash
    mvn clean spring-boot:run -Pdev9
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: default
    ```
    

> 注意: 同時要啟用兩個 Profiles 是可以的，透過 `-P` 搭配逗號分隔即可。例如你可以用以下命令來測試啟用的 Profile 名稱：`mvn help:active-profiles -Pdev,prod`

### 透過 Spring Profiles 切換不同的應用程式屬性檔

使用 Spring 框架的 Profiles 功能，有另外一個好處，那就是你可以不用把屬性都設定在 Maven 的 `pom.xml` 檔裡面，而是可以透過一種特殊的命名習慣，將應用程式屬性設定在不同的 `.properties` 檔案中。以下檔名規格請見註解說明：

```bash
# 這是預設的應用程式屬性檔，無論啟用哪一個設定，都會載入這個檔案中的屬性
application.properties

# 這是特定設定檔會套用的應用程式屬性檔，只有啟用的屬性檔會載入檔案中的屬性
application-{ProfileName}.properties
```

> 請注意: 在 `application` 檔名後面要接上 `-` (dash) 符號，然後才是接上你的 `ProfileName` 才是正確的命名規則。

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

1.  我們再加入一個 `dev9` 設定檔到 `pom.xml` 檔中
    
    ```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` 屬性)
    
    ```ini
    my.profile=@my.profile@
    spring.profiles.active=@spring.profiles.active@
    my.name=Will
    ```
    
    檔案 2: `src/main/resources/application-dev8.properties` (加入一個 `my.name` 屬性)
    
    ```ini
    my.name=John
    ```
    
    檔案 3: `src/main/resources/application-dev9.properties` (空白內容)
    
3.  修改 `HomeController` 的 `@Value` 標注，改注入 `spring.profiles.active` 屬性
    
    ```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("${spring.profiles.active}")
        private String myProfile;
    
        @Value("${my.name}")
        private String myName;
    
        @GetMapping("/")
        public String home() {
            return "Hello World: " + this.myName;
        }
    }
    ```
    
4.  測試執行
    
    請記得我們現在有 `3` 個設定檔，分別是 `default`, `dev8` 與 `dev9` 這三個。
    
    先嘗試不指定 Profile 的情況
    
    ```bash
    mvn clean spring-boot:run
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: Will
    ```
    
    再嘗試指定 Profile `dev8` 的情況
    
    ```bash
    mvn clean spring-boot:run -Pdev8
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: John
    ```
    
    最後嘗試指定 Profile `dev9` 的情況
    
    ```bash
    mvn clean spring-boot:run -Pdev9
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: Will
    ```
    

### 透過 Spring Profiles 載入不同的相依套件

透過 Spring Profiles 設定檔的方式進行組態設定，除了可以設定「屬性」之外，還能依據不同**設定檔**(Profile)來載入不同的 `<dependencies>` 相依套件，例如載入**相同套件**的**不同版本**(測試新舊版本)，或是**相同介面**但**不同套件**(不同資料庫驅動程式)之類的，這點真的很讚！ 👍

-   以下是**相同套件不同版本**的設定範例：
    
    ```xml
    <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 之後的相依套件資訊，可以執行以下命令：
    
    ```bash
    mvn dependency:tree -Pdev8
    ```
    
-   以下是**相同介面不同套件**的設定範例：
    
    ```xml
    <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` 類別
    
    ```java
    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` 類別
    
    ```java
    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` 類別
    
    ```java
    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` 服務
    
    ```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;
    
    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>` 定義
    
    ```xml
    <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`, `dev` 與 `prod` 這五個。
    
    嘗試指定 Profile `dev` 的情況
    
    ```bash
    mvn clean spring-boot:run -Pdev
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: Dev
    ```
    
    嘗試指定 Profile `prod` 的情況
    
    ```bash
    mvn clean spring-boot:run -Pprod
    ```
    
    ```bash
    $ curl localhost:8080
    Hello World: Prod
    ```
    

另一種實際的例子可以參考 [7\. Example: Separate Data Source Configurations Using Profiles](https://www.baeldung.com/spring-profiles#example-separate-data-source-configurations-using-profiles) 提供的範例。

> 由於 Maven Profiles 博大精深，還有更多用法可以參考 [Guide to Maven Profiles](https://www.baeldung.com/maven-profiles) 說明。

### 相關連結

-   [1.13. Environment Abstraction | Spring Core Technologies](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-environment)
-   [Spring Profiles | Baeldung](https://www.baeldung.com/spring-profiles)
-   [Guide to Maven Profiles | Baeldung](https://www.baeldung.com/maven-profiles)
-   [Properties with Spring and Spring Boot | Baeldung](https://www.baeldung.com/properties-with-spring)
-   [Using Environment Variables in Spring Boot's application.properties](https://www.baeldung.com/spring-boot-properties-env-variables)
-   [Understanding the spring profile - Stack Overflow](https://stackoverflow.com/a/72158286)
-   [Proper usage of Java -D command-line parameters - Stack Overflow](https://stackoverflow.com/a/5045637/910074)
-   [java - Can't access Maven properties with Spring - Stack Overflow](https://stackoverflow.com/a/36826113/910074)
-   [@Value 與 Spring EL](https://openhome.cc/Gossip/Spring/Value.html)
