➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Spring 項目中訂閱 Azure Service Bus批量下載檔案

Spring Cloud Config——使用 RDBMS(JDBC)存放配置

Table of contents

1 Spring Cloud Config 簡介

關於 Spring Cloud Config 既基本簡介可以喺呢篇度搵到:Spring Cloud Config——使用獨立既 Git 配置 repo - Spring Cloud Config 簡介

1.1 JDBC backend

今次呢篇係關於利用 JDBC backend,我地會用 RDBMS 傳統 DB 黎儲存所有配置資料。

2 建立 RDBMS

今次測試會用到 H2 in-memory DB 以及 MySQL,而 H2 我地只需要喺 code 度初始化就可以。

2.1 建立測試用 MySQL 連線

我地可以參考返呢篇文:Docker 基本操作 - MySQL
啟動左 Docker Desktop 之後,執行以下 command,喺 MySQL 度建立一個叫 scc 既 database:
docker container run -d --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=mysql -e MYSQL_PASSWORD=mysql -e MYSQL_DATABASE=scc -v "C:/docker-data/mysql:/var/lib/mysql" --name mysql mysql:latest

2.2 建立 table

先進入 Docker container 並且登入 MySQL 既 CLI,存取 scc database:
docker container exec -it mysql mysql -u"root" -p"root" scc
Spring Cloud Config 默認會用 properties table,但我地最好自定義 column 名,避免默認既 columns 帶有 reserved keywords,例如 keyvalue
SQL Server 既例子:
1CREATE TABLE properties ( 2 config_client_app_name VARCHAR(200) NOT NULL, 3 config_profile VARCHAR(200) NOT NULL, 4 config_label VARCHAR(200) NOT NULL, 5 config_key VARCHAR(200) NOT NULL, 6 config_value NVARCHAR(MAX) NULL 7);
MySQL 既例子:
1CREATE TABLE properties ( 2 config_client_app_name VARCHAR(200) NOT NULL, 3 config_profile VARCHAR(200) NOT NULL, 4 config_label VARCHAR(200) NOT NULL, 5 config_key VARCHAR(200) NOT NULL, 6 config_value VARCHAR(10000) NULL 7) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_0900_ai_ci;
註:如果想用 properties 以外既 table 名係可以既,稍後可以喺 config server 既配置度改條 SELECT SQL。

2.3 新增紀錄

INSERT INTO properties (config_client_app_name, config_profile, config_label, config_key, config_value) VALUES ('spring-cloud-config-client-demo', 'dev', 'master', 'my.prop', 'Hello MySQL!');

3 動手寫

3.1 Config server

Project structure:
  • src/main/java
    • /code
      • MainConfigServerApp.java
  • src/main/resources
    • application.yml
    • bootstrap.yml
    • data-h2.sql
    • schema-h2.sql

3.1.1 Maven dependencies

1<dependencyManagement> 2 <dependencies> 3 <dependency> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-dependencies</artifactId> 6 <version>2.7.11</version> 7 <type>pom</type> 8 <scope>import</scope> 9 </dependency> 10 <dependency> 11 <groupId>org.springframework.cloud</groupId> 12 <artifactId>spring-cloud-dependencies</artifactId> 13 <version>2021.0.7</version> 14 <type>pom</type> 15 <scope>import</scope> 16 </dependency> 17 </dependencies> 18</dependencyManagement> 19 20<dependencies> 21 <dependency> 22 <groupId>org.springframework.cloud</groupId> 23 <artifactId>spring-cloud-config-server</artifactId> 24 </dependency> 25 <dependency> 26 <groupId>org.springframework.boot</groupId> 27 <artifactId>spring-boot-starter-data-jdbc</artifactId> 28 </dependency> 29 30 <!-- MySQL --> 31 <dependency> 32 <groupId>com.mysql</groupId> 33 <artifactId>mysql-connector-j</artifactId> 34 </dependency> 35 36 <!-- SQL Server --> 37 <dependency> 38 <groupId>com.microsoft.sqlserver</groupId> 39 <artifactId>mssql-jdbc</artifactId> 40 <version>12.2.0.jre11</version> 41 </dependency> 42 43 <!-- H2 --> 44 <dependency> 45 <groupId>com.h2database</groupId> 46 <artifactId>h2</artifactId> 47 </dependency> 48</dependencies>

3.1.2 Java code

MainConfigServerApp.java
1@EnableConfigServer 2@SpringBootApplication 3public class MainConfigServerApp { 4 5 public static void main(String[] args) { 6 SpringApplication.run(MainConfigServerApp.class, args); 7 } 8}

3.1.3 Bootstrap 配置

bootstrap.yml
1encrypt: 2 key: michael ## or use the ENCRYPT_KEY environment variable 3 4spring: 5 profiles: 6 active: jdbc ## should be externalized 7 cloud: 8 config: 9 server: 10 encrypt: 11 enabled: true
註:一定要用 jdbc 既 Spring profile 先可以啟用到 JDBC backend。

3.1.4 Application 配置

我地喺原先既 Git backend 配置度再加上 JDBC backend 既配置。
application.yml
1spring: 2 datasource: 3# MySQL 4# url: jdbc:mysql://localhost:3306/scc 5# username: mysql 6# password: mysql 7# H2 8 url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_FILE=4 9 username: sa 10 password: 11 h2: 12 console: 13 enabled: true 14 path: /h2-console 15 sql: 16 init: 17 mode: always 18 platform: h2 19 cloud: 20 config: 21 server: 22 git: 23 clone-on-start: true 24 refresh-rate: 10 25 ignore-local-ssh-settings: true 26 uri: git@github.com:blackr1234/spring-cloud-config-demo.git 27 default-label: master 28 search-paths: 29 - "{application}" 30 host-key: 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' 31 host-key-algorithm: ecdsa-sha2-nistp256 32 private-key: | 33 -----BEGIN EC PRIVATE KEY----- 34 MHcCAQEEIPnupl8oxl0Wj6xfOd/PobBG48m3pVkmubPem1XSyexEoAoGCCqGSM49 35 AwEHoUQDQgAEUGO+DDpbpsgp3C+H68iTTaklmcnk2MSbbh4bwQVnMws09eqFqvA4 36 RzcTRtAXt2IWkq4JHUg6rtjDnc/0zwSQyQ== 37 -----END EC PRIVATE KEY----- 38 jdbc: 39 enabled: true 40 sql: "SELECT config_key, config_value FROM properties WHERE config_client_app_name = ? AND config_profile = ? AND config_label = ?" 41 order: 1
註:
  • spring.cloud.config.server.jdbc.sql
    • 條 SQL prepared statement 唔預你大改,因為啲 params(?)會塞既 values 係固定位置既。
    • 佢主要係畀你加減下啲 quotes,去令到條 SQL 可以適用喺我地用既 DB 度,例如 keyvalue 喺某啲版本既某啲 DB 可以係 reserved words,咁既話就一定要加 quotes。
    • 如果想用默認既 properties 以外既 table 名,可以喺度改。
    • 除非用最新版 Spring Cloud Config 4.0.1,否則 JDBC backend 係唔支援 default label,即係同 Git backend 唔同。

3.1.5 初始化 H2 in-memory DB 既配置檔

src/main/resources/schema-h2.sql
1CREATE TABLE properties ( 2 config_client_app_name VARCHAR(200) NOT NULL, 3 config_profile VARCHAR(200) NOT NULL, 4 config_label VARCHAR(200) NOT NULL, 5 config_key VARCHAR(200) NOT NULL, 6 config_value CLOB(10K) NULL 7);
src/main/resources/data-h2.sql
INSERT INTO properties (config_client_app_name, config_profile, config_label, config_key, config_value) VALUES ('spring-cloud-config-client-demo', 'dev', 'master', 'my.prop', 'Hello H2!');

3.2 Config client

至於 config client,我地可以完全用返之前寫落果個,詳情可以睇返呢篇:Spring Cloud Config——使用獨立既 Git 配置 repo - Config client

4 簡單測試

4.1 步驟

  1. 啟動 config server。
  2. 啟動 config client。
  3. 檢查 my.prop 既 value,應該係而家 DB 裡面既 value。
  4. 更改 DB 裡面既 my.prop 既 value。
  5. Call config client 既 /actuator/refresh API。
    • Response body 裡面會有一個 array,其中一個 string 就係 my.prop,意味住 my.prop 既 value 改變左。
  6. 檢查 my.prop 既 value,應該係我地改成既新 value。

4.2 檢查 property value

針對 my.prop,我地有以下既方法:
  1. Call config client 既 /actuator/env API,response body 會有 my.prop 既 value,呢個方法只需要配置好 Actuator 就可以用。
  2. 因為我地有特登寫 code 去做 logging,所以我地可以留意 config client 既 log 裡面 my.prop 既 value。

5 配置紀錄既應用次序

5.1 自己發現到既 bug

我喺 2023-01-07 發現到 spring-cloud-config-server 版本 4.0.0 或以前既 JDBC backend 對於配置紀錄既應用次序同 Git backend 有唔同,之後 Spring Cloud 項目既維護人員確認左個 bug,然後喺新版本 4.0.1 度 fix。

5.1.1 問題核心

Git backend 既實現畀到出黎既次序係:
  1. application
  2. {spring.application.name}
  3. application-{profile}
  4. {spring.application.name}-{profile}
但當我地使用 JDBC backend,個次序就會變左:
  1. application
  2. application-{profile}
  3. {spring.application.name}
  4. {spring.application.name}-{profile}
呢個 bug 一直存在喺 JDBC backend 既 code,但唔影響 Git backend。

5.1.2 即時解決方法

確定適用既 Spring Boot 版本:2.7.1
如果等唔切 spring-cloud-config-server 推出新版本,或者因為某啲原因而唔想用新版本,咁喺使用舊版本既情況下,我地可以覆蓋 Spring 有問題既 Java class。
以下係改好左既 org.springframework.cloud.config.server.environment.JdbcEnvironmentRepository,我地只要將佢放喺我地既 config server 既 src/main/java 既對應 package 度,個 Java app 運行既時候就會用我地果個版本既 Java class。
唯一既改動就係將 findOne method 既 2 個 for loops 調轉。
1package org.springframework.cloud.config.server.environment; 2 3/** 4 * <p>This class is a duplicate of the same class from Spring Cloud Config. 5 * This version fixes a bug in the order of queries with application names and profiles.</p> 6 * 7 * @author Dave Syer 8 * @author Michael Chung 9 * @see <a href="https://github.com/spring-cloud/spring-cloud-config/issues/2205">Spring Cloud Config issue #2205</a> 10 * @see <a href="https://github.com/spring-cloud/spring-cloud-config/pull/2207/commits/cc5aae457eef2ab5925669b4c608bbb3f003e694">Spring Cloud Config issue #2207 - commit</a> 11 */ 12public class JdbcEnvironmentRepository implements EnvironmentRepository, Ordered { 13 14 // ... 15 16 @Override 17 public Environment findOne(String application, String profile, String label) { 18 19 // ... 20 21 // XXX fixed the order 22 for (String env : envs) { 23 for (String app : applications) { 24 try { 25 Map<String, Object> next = this.jdbc.query(this.sql, this.extractor, app, env, label); 26 if (next != null && !next.isEmpty()) { 27 environment.add(new PropertySource(app + "-" + env, next)); 28 } 29 } 30 catch (DataAccessException e) { 31 if (!failOnError) { 32 if (logger.isDebugEnabled()) { 33 logger.debug("Failed to retrieve configuration from JDBC Repository", e); 34 } 35 } 36 else { 37 throw e; 38 } 39 } 40 } 41 } 42 return environment; 43 } 44}
註:
  • 呢個方法都係試出黎。
  • Spring 既 class loading 複雜,而根據經驗,新舊版本既實現都可以好唔同。
    • 並唔能夠保證舊版或者新版既 Spring Boot 項目甚或冇用 Spring 既 Maven 項目都一樣可以咁做黎覆蓋第三方 library 既 source code。
  • 但至少試過喺呢個 Spring Boot 版本下打包成 JAR 之後都可以成功覆蓋到。

6 連線容錯測試

根據測試,JDBC backend 並唔容許 DB 連線失敗,因為佢冇用 cache,所以當個 config DB 有 downtime,config client 係有可能啟動唔到。

6.1 使用 JDBC backend 而 DB 連線失敗

模擬連線失敗場景:
  1. 啟動 MySQL Docker container。
  2. 啟動 config server。
  3. 啟動 config client。
  4. 呢個時候,我地可以見到 config server 既 log 度有 SELECT SQL 既 logs。
  5. 暫停 MySQL Docker container。
  6. 重新啟動 config client,可以見到 config client 啟動既時候停左喺度一陣之後,就因為喺 config server 度拎唔到配置而繼續執行,而視乎個 JAR 檔裡面既配置能唔能夠滿足到啲 Spring beans 既需要,個 config client 有可能會啟動失敗。咁都係因為 config server 冇任何 cache,所以佢每次都會問 DB 拎配置紀錄。

7 用返 Git backend

當我地已經用緊 JDBC backend,但係有需要用返 Git 既話,就要重新執行過個 config server。
我地需加上以下既配置去 disable datasource 既 auto-configuration:
spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
另外,我地亦都要:
  • 唔用 jdbc Spring profile。
  • 移除 spring.datasource 既相關配置。
  • spring.cloud.config.server.jdbc.enabled 設置成 false