上一篇文章講了 Config Server 的設定,這篇就來講怎麼在 client 端正確地拉和使用這些設定。有不少人在這邊踩過坑,主要都是因為搞不清楚 bootstrap.ymlapplication.yml 的區別。

bootstrap.yml vs application.yml

這是最關鍵的一點,千萬別搞反了。

  • bootstrap.yml - 用來設定 Spring Cloud 相關的東西,比如 Config Server 的位置。它的載入優先序比 application.yml 更高
  • application.yml - 普通的應用設定

如果你把 Config Server 的設定寫在 application.yml,Spring 會先載入 application.yml,但這時候還沒有連到 Config Server,所以設定會不全。一定要寫在 bootstrap.yml。

Config Client 的基本設定

在你的 Spring Boot 應用裡,先加依賴:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>

然後在 bootstrap.yml 裡面設定:

1
2
3
4
5
6
7
8
spring:
application:
name: user-service
profiles:
active: dev
cloud:
config:
uri: http://localhost:8888

各個設定項目的含義

  • spring.application.name - 應用的名稱,Config Server 會用這個名稱去找對應的設定檔
  • spring.profiles.active - 啟用的 profile,比如 dev、prod
  • spring.cloud.config.uri - Config Server 的位址

如果你想要 Config Server 的路徑更複雜,可以這樣設定:

1
2
3
4
5
6
7
spring:
cloud:
config:
uri: http://config-server.example.com:8888
name: user-service
profile: dev
label: main # 如果 Config Server 用 Git,這就是 branch 名稱

用 @ConfigurationProperties 綁定設定

與其用 @Value 逐個注入,不如用 @ConfigurationProperties 把相關的設定綁在一個 POJO class 上面。假設 Config Server 有這樣的設定:

1
2
3
4
5
6
7
8
9
10
# user-service-dev.yml
app:
database:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: password
cache:
type: redis
host: localhost
port: 6379

你可以建立一個設定 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {

private DatabaseProperties database;
private CacheProperties cache;

// getter/setter
public static class DatabaseProperties {
private String url;
private String username;
private String password;

// getter/setter
}

public static class CacheProperties {
private String type;
private String host;
private int port;

// getter/setter
}
}

然後在 service 裡面就可以注入:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {

@Autowired
private AppProperties appProperties;

public void connect() {
String url = appProperties.getDatabase().getUrl();
String host = appProperties.getCache().getHost();
System.out.println("DB: " + url + ", Cache: " + host);
}
}

@ConfigurationProperties 和 @Value 的區別

特性 @ConfigurationProperties @Value
適合用途 多個相關設定的綁定 單個設定值
前綴支援 支援,整個 prefix 下的都會綁定 不支援
複雜物件 支援巢狀物件 只能用 SpEL
驗證 可以加 @Validated 不支援
設定來源 environment + Config Server environment

用 SpEL 的例子:

1
2
3
4
5
6
7
8
9
@Service
public class UserService {

@Value("${app.database.url}")
private String dbUrl;

@Value("${app.cache.host:localhost}") // 預設值是 localhost
private String cacheHost;
}

動態更新設定(不重啟應用)

有時候你想要改設定不重啟應用。Spring Cloud Config 提供了一個功能——用 @RefreshScope + POST /actuator/refresh 來做這個。

首先加 actuator 依賴:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然後在 Config Client 的 application.yml 裡面開啟 refresh endpoint:

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: refresh

在你想要支援熱更新的 Bean 上加 @RefreshScope

1
2
3
4
5
6
7
8
9
10
11
@Component
@RefreshScope
public class FeatureFlagService {

@Value("${features.new-ui:false}")
private boolean newUiEnabled;

public boolean isNewUiEnabled() {
return newUiEnabled;
}
}

改完設定檔後,呼叫這個 endpoint 就能更新:

1
curl -X POST http://localhost:8080/actuator/refresh

應用就會從 Config Server 重新拉設定,不用重啟。

用 @ConfigurationProperties + @RefreshScope

如果你用 @ConfigurationProperties@RefreshScope 組合,會更方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
@ConfigurationProperties(prefix = "features")
@RefreshScope
public class FeatureProperties {

private boolean newUiEnabled;
private boolean betaFeatureEnabled;

// getter/setter
}

@Service
public class UserService {

@Autowired
private FeatureProperties featureProperties;

public void doSomething() {
if (featureProperties.isNewUiEnabled()) {
// 新的邏輯
} else {
// 舊的邏輯
}
}
}

開發 vs 生產的設定切換

通常開發和生產會有不同的 Config Server。在 bootstrap.yml 裡面用環境變數切換:

1
2
3
4
5
6
7
8
spring:
application:
name: user-service
profiles:
active: ${PROFILE:dev}
cloud:
config:
uri: ${CONFIG_SERVER_URI:http://localhost:8888}

然後啟動應用時傳入環境變數:

1
2
3
4
5
6
7
8
9
# 開發環境
export PROFILE=dev
export CONFIG_SERVER_URI=http://localhost:8888

# 生產環境
export PROFILE=prod
export CONFIG_SERVER_URI=http://config.production.com:8888

java -jar user-service.jar

設定載入的優先順序

Spring Cloud Config Client 的設定載入優先順序是這樣的(高到低):

  1. 環境變數
  2. Config Server 的設定
  3. application.yml / application-{profile}.yml
  4. bootstrap.yml / bootstrap-{profile}.yml
  5. 預設值

也就是說,如果你在環境變數裡設定了某個值,它會覆蓋 Config Server 的值。

常見的坑

1. 把設定寫在 application.yml 而不是 bootstrap.yml

1
2
3
4
5
# 錯誤:application.yml
spring:
cloud:
config:
uri: http://localhost:8888

Config Server 的設定一定要在 bootstrap.yml。

2. 忘記加 spring-cloud-config-client 依賴

沒有這個依賴,Spring 根本不會去連 Config Server。

3. @ConfigurationProperties 沒有指定 prefix

1
2
3
4
5
6
7
8
9
// 錯誤
@ConfigurationProperties
public class AppProperties {
}

// 正確
@ConfigurationProperties(prefix = "app")
public class AppProperties {
}

4. 熱更新時忘記加 @RefreshScope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 錯誤:改設定後不會生效
@Component
public class FeatureService {
@Value("${feature.enabled}")
private boolean enabled;
}

// 正確
@Component
@RefreshScope
public class FeatureService {
@Value("${feature.enabled}")
private boolean enabled;
}

完整例子

Config Server 的 user-service-dev.yml

1
2
3
4
5
6
7
8
9
server:
port: 8001

app:
name: user-service
database:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: password

Client 的 bootstrap.yml

1
2
3
4
5
6
7
8
spring:
application:
name: user-service
profiles:
active: dev
cloud:
config:
uri: http://localhost:8888

Client 的 application.yml

1
2
3
logging:
level:
root: INFO

Client 的設定 class:

1
2
3
4
5
6
7
8
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name;
private DatabaseConfig database;

// getter/setter
}

Client 啟動時,Spring 會:

  1. 讀 bootstrap.yml,知道要去連 Config Server
  2. 連到 Config Server,拉 user-service-dev.yml
  3. 合併兩份設定
  4. 初始化 Bean

重點整理

  • 設定一定要在 bootstrap.yml,不是 application.yml
  • @ConfigurationProperties 綁定相關設定,更整潔
  • @RefreshScope + /actuator/refresh 可以不重啟更新設定
  • Config Server 的設定優先序比本地設定高
  • 環境變數優先序最高

搞清楚這些概念後,就能輕鬆管理各個環境的設定了。