組態伺服器


組態這件事,也不是一開始就變得複雜的,應用程式中本來寫死的某個資訊,後續因為程式中多個地方要用到這個資訊,於是以一個變數來代表,這個變數會是公開的,也許以類別或模組的常數存在,日後只要更動共用的資訊,只要更動這個變數就可以了,一開始就都是這樣而已…

然後,為了要讓客戶也可以自行更改組態,總不能叫他們修改後還得再重新編譯之類的,於是組態移出了程式碼,使用 properties、xml、json 或其他方式儲存,客戶可以修改組態來源,應用程式讀取這些組態來源,不用重新編譯等動作。

不同的應用程式,也許會共用某些組態資訊,例如,多個應用程式可能需要郵件服務,而郵件服務的組態資訊都是相同的,這時該怎麼做?在多個應用程式之間直接複製組態是很簡單的一件事,然而,這就跟複製程式碼一樣,未來若組態變更,你就得記得逐一更新應用程式的組態設定。

簡單來說,組態跟程式碼管理很類似,會需要視應用程式的需求而重構,決定採用哪種組態方式,更進一步地,也會有版本控制之類的需求,衍生出專用的組態供應者,以及對應的組態消費者,當組態的供應與消費需要透過網路之時,組態供應者就會以伺服器的形式實現,也就需要決定組態交換時的協定、格式等,以便組態消費者進行組態的取得、更新等。

Spring Cloud 提供了組態管理方案,在 Spring Boot 的抽象下,可以使用自家的 Spring Cloud Config 伺服器,或者是整合其他既有的組態管理方案。

直接來看個簡單的專案,認識一下如何運用 Spring Cloud Config 來建立組態伺服器,使用 Spring Tool Suite 的話,可以於建立專案時,選擇 Config Server 的 Starter,在 build.gradle 中會有以下預設的相依:

...略
dependencies {
    implementation('org.springframework.cloud:spring-cloud-config-server')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}
...略

接著來決定伺服器的埠號與組態來源,可以在 application.properties 中加入:

server.port=8888
spring.profiles.active=native

為了讓事情一開始簡單一些,專案使用本機上的組態檔案(預設使用 Git Repository 作為來源),這需要設置 spring.profiles.active=native,預設可放置組態的位置有類別路徑、類別路徑中的 config 目錄、專案根目錄、專案根目錄中的 config 目錄,如果要使用其他目錄的話,可以透過 spring.cloud.config.server.native.searchLocations 來設置,可以有多個路徑,使用逗號區隔。

在這邊選擇在類別路徑中的 config 目錄建立組態檔,例如建立一個 gossip.properties:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/c:/workspace/gossip/gossip
spring.datasource.username=caterpillar
spring.datasource.password=12345678

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=yourname@gmail.com
spring.mail.password=yourpassword
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

這是預設的組態檔,gossip 名稱是自取的,如果有 Profile 的需求,可以有 name-profile.properties 的組態檔,例如,在這邊建立一個 gossip-dev.properties:

spring.datasource.url=jdbc:h2:tcp://localhost/c:/workspace/gossip/test
spring.datasource.username=test
spring.datasource.password=test

為了啟用組態伺服器,必須在 Spring Boot 啟動類別上加註 @EnableConfigServer

package cc.openhome;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigsvrApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigsvrApplication.class, args);
    }
}

接著啟動專案,請求 http://localhost:8888/gossip/default 的話,就會看到底下的 JSON(為了便於觀看,已排版過):

{
    "name": "gossip",
    "profiles": [
        "default"
    ],
    "label": null,
    "version": null,
    "state": null,
    "propertySources": [
        {
            "name": "classpath:/config/gossip.properties",
            "source": {
                "spring.datasource.driver-class-name": "org.h2.Driver",
                "spring.datasource.url": "jdbc:h2:tcp://localhost/c:/workspace/gossip/gossip",
                "spring.datasource.username": "caterpillar",
                "spring.datasource.password": "12345678",
                "spring.mail.host": "smtp.gmail.com",
                "spring.mail.port": "587",
                "spring.mail.username": "yourname@gmail.com",
                "spring.mail.password": "yourpassword",
                "spring.mail.properties.mail.smtp.auth": "true",
                "spring.mail.properties.mail.smtp.starttls.enable": "true"
            }
        }
    ]
}

這傳回了預設的 gossip.properties 中組態等資訊,如果請求特定的 Profile,例如 http://localhost:8888/gossip/dev,那麼會傳回預設的 gossip.properties,以及 gossip-dev.properties 組態相關資訊。

{
    "name": "gossip",
    "profiles": [
        "dev"
    ],
    "label": null,
    "version": null,
    "state": null,
    "propertySources": [
        {
            "name": "classpath:/config/gossip-dev.properties",
            "source": {
                "spring.datasource.url": "jdbc:h2:tcp://localhost/c:/workspace/gossip/test",
                "spring.datasource.username": "test",
                "spring.datasource.password": "test"
            }
        },
        {
            "name": "classpath:/config/gossip.properties",
            "source": {
                "spring.datasource.driver-class-name": "org.h2.Driver",
                "spring.datasource.url": "jdbc:h2:tcp://localhost/c:/workspace/gossip/gossip",
                "spring.datasource.username": "caterpillar",
                "spring.datasource.password": "12345678",
                "spring.mail.host": "smtp.gmail.com",
                "spring.mail.port": "587",
                "spring.mail.username": "yourname@gmail.com",
                "spring.mail.password": "yourpassword",
                "spring.mail.properties.mail.smtp.auth": "true",
                "spring.mail.properties.mail.smtp.starttls.enable": "true"
            }
        }
    ]
} 

如果你更新了組態資訊,請求伺服器傳回的就會是更新後的資訊,由於使用 JSON 傳回,只要客戶端能消化 JSON,就可以取得其中的組態內容,當然,如果客戶端是個 Spring 應用程式,Spring 提供了 Config Client 的方案,可以直接與 Spring 整合。

你可以在 configsvr 找到以上的範例專案。