SpringBoot + @RefreshScope:動態刷新配置的終極指南!
兄弟們,今天咱們來聊一個在實際開發中能幫你少掉很多頭發的知識點 ——SpringBoot 里的 @RefreshScope 動態刷新配置。
我猜你肯定遇到過這種情況:線上服務跑的好好的,突然產品經理跑過來說 “那個某某配置能不能改一下?客戶那邊著急要”。這時候你心里是不是咯噔一下?因為按照常規操作,改配置就得重啟服務啊!可線上服務哪能說重啟就重啟?重啟意味著服務中斷,用戶可能會投訴,老板可能會找你談話,想想都頭大。
以前我剛入行的時候,就因為這個踩過坑。有一次線上要改個短信模板的配置,我傻乎乎地直接重啟了服務,結果導致幾分鐘內用戶收不到驗證碼,投訴電話都快被打爆了。當時我站在老板辦公室門口,腿都在抖。后來才知道,原來 SpringBoot 早就給咱們準備了 “神器”——@RefreshScope,能讓配置改了之后不用重啟服務就生效。要是早知道這個,我也不至于當年那么狼狽了。
好了,不聊我的黑歷史了,咱們正式進入正題。這篇文章咱們就從 “是什么”“怎么用”“原理是啥”“進階玩法”“坑在哪” 這幾個方面,把 @RefreshScope 給講透了。保證全程大白話,沒有那些繞來繞去的專業術語,就算是剛接觸 SpringBoot 的小伙伴,也能看得明明白白。
一、先搞懂:@RefreshScope 到底是個啥?
咱們先從最基礎的開始,@RefreshScope 到底是個什么東西?
簡單來說,它就是 SpringCloud 里提供的一個 “作用域” 注解(跟咱們平時用的 Singleton、Prototype 這些作用域類似),只不過它的特殊之處在于:被它標記的 Bean,在配置發生變化的時候,能被 “重新創建”,從而加載新的配置。
可能有人會問:“不對啊,我用的是 SpringBoot,不是 SpringCloud,能用上這個注解嗎?” 這里要跟大家澄清一下,@RefreshScope 雖然是 SpringCloud 里的組件,但 SpringBoot 項目只要引入對應的依賴,也能正常使用。就像咱們吃火鍋,雖然火鍋底料是四川的,但不管你在哪個城市,只要買到底料,就能自己煮火鍋,一個道理。
再舉個通俗的例子:你家有個冰箱,里面放著牛奶(這牛奶就相當于 Bean 里的配置)。以前冰箱是 “死的”,你想換牛奶,就得把冰箱斷電(重啟服務)才能拿出來換。而 @RefreshScope 就相當于給冰箱裝了個 “智能門”,你不用斷電,打開門就能把舊牛奶換成新牛奶,冰箱還能正常工作,是不是很方便?
二、實戰第一步:把環境搭起來
要想用 @RefreshScope,首先得把環境搭好。這一步很關鍵,要是依賴引錯了或者版本不對,后面咋折騰都沒用。咱們一步一步來,別著急。
2.1 引入必要的依賴
首先,你的 SpringBoot 項目得引入 SpringCloud 的相關依賴。這里要注意一個點:SpringBoot 和 SpringCloud 的版本是有對應關系的,不能隨便亂搭,不然會出現各種兼容性問題,到時候你排查問題都能排查到懷疑人生。
我給大家列一個常用的版本組合(截至目前比較穩定的):
- SpringBoot 版本:2.7.x
- SpringCloud 版本:2021.0.x(也就是 Alibaba Cloud 2021.0.4.0 版本,很多國內公司都用這個)
接下來,在 pom.xml 里加依賴。首先是 SpringCloud 的依賴管理(這個相當于定好 “規矩”,后面引具體依賴的時候就不用寫版本號了):
<dependencyManagement>
<dependencies>
<!-- SpringCloud 依賴管理 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 要是用Alibaba Cloud的話,加這個 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>然后,引入 @RefreshScope 所在的依賴 ——spring-cloud-starter-bootstrap:
<dependencies>
<!-- SpringBoot Web依賴(要是你的項目是Web項目,就加這個) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- @RefreshScope核心依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 配置中心依賴(要是用Nacos、Apollo這些配置中心,就加對應的依賴,這里以Nacos為例) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>這里要跟大家說一下,為什么要引入 spring-cloud-starter-bootstrap?因為在 SpringBoot 2.4.x 之后,默認不加載 bootstrap.properties/bootstrap.yml 文件了,而 @RefreshScope 要配合配置中心使用的話,通常會用到這個文件來配置配置中心的信息。要是你不用配置中心,只是本地測試,那這個依賴也得加,因為 @RefreshScope 的核心功能就包含在這個依賴里。
2.2 配置文件準備
接下來咱們搞配置文件。這里分兩種情況:一種是本地測試(不用配置中心),一種是結合配置中心(比如 Nacos)。咱們都講一下,大家根據自己的情況選。
2.2.1 本地測試配置(不用配置中心)
本地測試的話,咱們用 application.yml 就行。先寫個簡單的配置,比如一個 “用戶問候語” 的配置:
spring:
application:
name: refresh-scope-demo
# 自定義配置
user:
greeting: "Hello, 歡迎使用@RefreshScope!當前時間:${spring.application.name}"這里的${spring.application.name}是引用了 SpringBoot 的內置配置,主要是為了后面測試的時候能看出來配置確實刷新了。
2.2.2 結合 Nacos 配置中心(實際開發常用)
要是在實際開發中,咱們一般會用配置中心(比如 Nacos)來管理配置,這樣改配置更方便,還能統一管理。那配置就得這么寫:
首先,創建 bootstrap.yml 文件(這個文件比 application.yml 加載優先級高),配置 Nacos 的信息:
spring:
application:
name: refresh-scope-demo
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos服務地址,要是線上的話就填線上地址
file-extension: yaml # 配置文件格式,yaml或者properties
group: DEFAULT_GROUP # 配置分組,默認是DEFAULT_GROUP
namespace: # 命名空間,要是沒創建的話可以不填然后,在 Nacos 控制臺里創建對應的配置文件。配置文件的 “Data ID” 要按照 “({spring.application.name}-){profile}.${file-extension}” 的格式來,比如咱們這里就是 “refresh-scope-demo-dev.yaml”(dev 是環境,要是生產環境就是 prod)。在 Nacos 里的配置內容跟本地的差不多:
user:
greeting: "Hello, 歡迎使用@RefreshScope!當前時間:${spring.application.name},來自Nacos配置中心"這樣,項目啟動的時候就會從 Nacos 拉取配置了。
三、核心操作:@RefreshScope 怎么用?
環境搭好了,接下來就是最核心的部分 —— 怎么用 @RefreshScope 實現動態刷新配置。這部分很簡單,就幾步操作,咱們一步步來。
3.1 給 Bean 加 @RefreshScope 注解
首先,咱們要創建一個配置類,用來讀取配置。這個配置類上要加兩個注解:@ConfigurationProperties(用來綁定配置)和 @RefreshScope(用來開啟動態刷新)。
比如咱們創建一個 UserConfig 類:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
// 把這個類交給Spring管理
@Component
// 綁定配置前綴,這里是"user",對應配置文件里的"user"節點
@ConfigurationProperties(prefix = "user")
// 開啟動態刷新功能,關鍵注解!
@RefreshScope
public class UserConfig {
// 對應配置文件里的"user.greeting"
private String greeting;
// Getter和Setter方法一定要有!不然配置綁不上,別問我怎么知道的...
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}這里有個坑要跟大家說一下:@ConfigurationProperties 注解綁定配置的時候,一定要有 Getter 和 Setter 方法!我以前就犯過這個錯,沒寫 Setter 方法,結果配置一直綁不上,查了半天都沒找到問題,最后才發現是少了 Setter 方法。所以大家一定要記住,這個不能忘!
3.2 寫個接口測試一下
接下來,咱們寫一個 Controller,用來測試配置是否能動態刷新。這樣咱們就能通過訪問接口,看到配置的變化了。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RefreshScopeController {
// 注入咱們剛才寫的UserConfig
@Autowired
private UserConfig userConfig;
// 寫個接口,返回當前的問候語配置
@GetMapping("/getGreeting")
public String getGreeting() {
// 這里加個當前時間戳,方便咱們看出來配置確實刷新了
return "當前配置:" + userConfig.getGreeting() + " | 訪問時間:" + System.currentTimeMillis();
}
}3.3 啟動項目,測試效果
現在咱們啟動項目,然后訪問接口 “http://localhost:8080/getGreeting”(要是你改了端口,就用你改后的端口)。
第一次訪問的時候,返回的結果應該是這樣的(本地測試的情況):
當前配置:Hello, 歡迎使用@RefreshScope!當前時間:refresh-scope-demo | 訪問時間:1667890123456接下來,咱們改一下配置文件里的 “user.greeting”。比如改成:
user:
greeting: "Hi, 我是修改后的問候語!當前時間:${spring.application.name}"改完之后,要是不用 @RefreshScope 的話,咱們得重啟項目才能看到變化。但現在咱們用了 @RefreshScope,是不是直接訪問接口就能看到變化了?等等,這里要注意!要是你用的是本地配置文件(application.yml),改完配置之后,還需要 “觸發” 一下刷新操作。因為 SpringBoot 不會主動去監聽本地配置文件的變化(除非你加了額外的監聽)。那怎么觸發呢?
有兩種方法:
方法 1:調用 SpringBoot 的 Actuator 端點
首先,咱們得在 pom.xml 里引入 Actuator 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>然后,在配置文件里開啟 refresh 端點:
management:
endpoints:
web:
exposure:
include: refresh # 暴露refresh端點,多個的話用逗號分隔,比如"refresh,health,info"然后,咱們用 Postman 或者 curl 工具,發送一個 POST 請求到 “http://localhost:8080/actuator/refresh”。請求發送成功之后,再訪問 “/getGreeting” 接口,就能看到配置已經更新了:
當前配置:Hi, 我是修改后的問候語!當前時間:refresh-scope-demo | 訪問時間:1667890456789是不是很神奇?不用重啟服務,配置就生效了!
方法 2:要是用了 Nacos 配置中心
要是你用的是 Nacos 配置中心,那就更方便了。你只需要在 Nacos 控制臺里修改對應的配置,然后點擊 “發布” 按鈕。發布之后,Nacos 會主動通知 SpringBoot 項目配置變了,項目會自動觸發刷新,不用咱們手動調用 refresh 端點。這時候你再訪問接口,就能看到新的配置了。
我之前在項目里用 Nacos 的時候,就試過這個功能。當時我在 Nacos 里改了個支付回調的地址,改完之后點發布,然后立馬訪問接口,發現配置已經變了,當時心里就一個字:“爽!” 再也不用跟運維大哥申請重啟服務了。
四、深入原理:@RefreshScope 是怎么做到的?
咱們用著是爽了,但作為一個有追求的 Java 程序員,不能只知道 “怎么用”,還得知道 “為什么這么用”,也就是 @RefreshScope 的原理。這部分稍微有點技術含量,但我會用大白話給大家講明白,保證不讓你卡住。
4.1 先回顧一下 Spring Bean 的作用域
咱們先回顧一下 Spring 里 Bean 的作用域。平時咱們用的最多的是 Singleton(單例),也就是一個 Bean 在 Spring 容器里只有一個實例,從容器啟動到容器關閉,這個實例一直存在。
而 @RefreshScope 是一種 “自定義作用域”,它的名字叫 “refresh”。被這個作用域標記的 Bean,跟 Singleton 不一樣,它不是一直存在的,而是會在某些情況下被 “銷毀” 然后 “重新創建”。
4.2 @RefreshScope 的核心原理:“銷毀重建”
@RefreshScope 的核心原理其實很簡單,就四個字:“銷毀重建”。具體來說,分為以下幾步:
- Bean 初始化的時候:當 Spring 容器啟動,創建被 @RefreshScope 標記的 Bean 的時候,不會直接創建 Bean 的實例,而是創建一個 “代理對象”(可以理解為一個 “替身”)。這個代理對象會保存在 Spring 容器里,當你需要用這個 Bean 的時候,其實用的是這個代理對象。
- 配置發生變化的時候:當配置中心(比如 Nacos)的配置變了,或者咱們調用了 refresh 端點之后,Spring 會收到一個 “配置刷新事件”(RefreshEvent)。
- 銷毀舊的 Bean 實例:收到 RefreshEvent 之后,Spring 會找到所有被 @RefreshScope 標記的 Bean,然后把這些 Bean 的 “真實實例” 給銷毀掉(注意:代理對象還在)。
- 創建新的 Bean 實例:當你下次再訪問這個 Bean 的時候(比如調用 Controller 里的接口,用到了 UserConfig),代理對象發現原來的真實實例已經被銷毀了,就會重新創建一個新的 Bean 實例。在創建新實例的時候,會從新的配置里讀取值,這樣新的實例里的配置就是最新的了。
咱們用個比喻來理解一下:你去餐廳吃飯,服務員(代理對象)負責給你上菜。第一次上菜的時候,服務員去廚房(Spring 容器)拿了一盤菜(舊的 Bean 實例)給你。后來廚房說這道菜的配方改了(配置變了),就把原來的菜倒掉了(銷毀舊實例)。當你下次再要這道菜的時候,服務員又去廚房拿了一盤新配方的菜(新的 Bean 實例)給你。在這個過程中,服務員一直都在,沒變過,變的是他拿給你的菜。
4.3 關鍵組件:RefreshScope 類
@RefreshScope 注解本身其實沒多少代碼,真正實現邏輯的是它背后的 “RefreshScope 類”。這個類繼承了 AbstractRefreshableScope,實現了 Scope 接口,是 @RefreshScope 功能的核心。
咱們來看一下 RefreshScope 類里的幾個關鍵方法:
- get 方法:這個方法是用來獲取 Bean 實例的。當你需要用 Bean 的時候,Spring 會調用這個方法。在這個方法里,會先檢查有沒有現成的 Bean 實例。如果有,就返回;如果沒有(或者舊的實例被銷毀了),就重新創建一個新的實例。
- destroy 方法:這個方法是用來銷毀 Bean 實例的。當收到 RefreshEvent 的時候,就會調用這個方法,把舊的 Bean 實例銷毀掉。
- refreshAll 方法:這個方法是用來刷新所有 @RefreshScope 標記的 Bean 的。當調用 refresh 端點的時候,其實就是調用了這個方法。
簡單來說,RefreshScope 類就像一個 “Bean 管理器”,負責管理被 @RefreshScope 標記的 Bean 的創建和銷毀。
4.4 為什么需要代理對象?
這里可能有人會問:“為什么要創建代理對象?直接創建真實實例不行嗎?”
其實原因很簡單:為了保證 “無縫切換”。要是沒有代理對象,當舊的 Bean 實例被銷毀之后,你再訪問這個 Bean 的時候,就會找不到實例,出現空指針異常。而有了代理對象之后,代理對象會幫你處理 “找不到實例就重新創建” 的邏輯,你根本感覺不到 Bean 實例已經被換了,就像什么都沒發生一樣。
這就好比你家里的水電,水電公司要換水管或者電線的時候,會有一個 “總開關”(代理對象)。在換的時候,總開關會先把舊的關掉,然后換上新的,換完之后再打開。在這個過程中,你家里的電器(相當于調用 Bean 的代碼)不用動,等換完之后,打開開關就能繼續用,完全感覺不到中間的切換過程。
五、進階玩法:@RefreshScope 的高級用法
咱們學會了基礎用法之后,再來看看 @RefreshScope 的一些進階玩法。這些玩法在實際開發中會經常用到,能幫你解決更多復雜的問題。
5.1 結合 @ConfigurationProperties 使用(推薦)
咱們前面已經用了 @ConfigurationProperties 來綁定配置,這里再跟大家強調一下,這種方式是推薦的。因為 @ConfigurationProperties 能很方便地把配置文件里的多個屬性綁定到一個類上,而且支持類型轉換(比如把字符串轉成整數、布爾值等)。
比如咱們有一個 “訂單配置”,包含訂單超時時間、最大訂單數量等:
order:
timeout: 30 # 訂單超時時間,單位:分鐘
max-count: 100 # 最大訂單數量
enable-notify: true # 是否開啟訂單通知然后寫一個 OrderConfig 類:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "order")
@RefreshScope
publicclass OrderConfig {
private Integer timeout;
private Integer maxCount;
privateBoolean enableNotify;
// Getter和Setter方法
// ...(省略Getter和Setter,實際開發中一定要寫)
}這樣,當配置里的 order.timeout、order.max-count、order.enable-notify 這些值變了之后,OrderConfig 的實例會被重新創建,新的實例里的屬性就是最新的值。這里要注意一個點:@ConfigurationProperties 的 prefix 屬性要跟配置文件里的節點對應上,而且屬性名要跟配置里的 key 對應(比如配置里的 max-count,對應類里的 maxCount,Spring 會自動處理 “橫杠轉駝峰”)。
5.2 給 @Bean 方法加 @RefreshScope
除了給 @Component 標記的類加 @RefreshScope,咱們還可以給 @Bean 方法加 @RefreshScope。這種情況適合那些通過 @Bean 方法創建的 Bean。
比如咱們有一個 RedisTemplate 的 Bean,它的配置(比如 Redis 的地址、密碼)是從配置文件里讀的,咱們想讓 Redis 的配置變了之后不用重啟服務,就可以給 @Bean 方法加 @RefreshScope:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
publicclass RedisConfig {
// 從配置文件里讀取Redis地址
@Value("${spring.redis.host}")
privateString redisHost;
// 從配置文件里讀取Redis端口
@Value("${spring.redis.port}")
private Integer redisPort;
// 從配置文件里讀取Redis密碼
@Value("${spring.redis.password:}")
privateString redisPassword;
// 給@Bean方法加@RefreshScope,這樣RedisTemplate會隨著配置變化而刷新
@Bean
@RefreshScope
public LettuceConnectionFactory lettuceConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
if (!"".equals(redisPassword)) {
config.setPassword(redisPassword);
}
returnnew LettuceConnectionFactory(config);
}
@Bean
// 這里不用加@RefreshScope,因為RedisTemplate依賴的LettuceConnectionFactory已經加了
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 設置序列化方式(省略,實際開發中要配置)
return redisTemplate;
}
}這里要注意:只要給 LettuceConnectionFactory 的 @Bean 方法加 @RefreshScope 就行,RedisTemplate 的 @Bean 方法不用加。因為 RedisTemplate 依賴 LettuceConnectionFactory,當 LettuceConnectionFactory 刷新之后,RedisTemplate 會自動使用新的 LettuceConnectionFactory。我之前在項目里就這么配置過 Redis,有一次 Redis 集群遷移,需要改 Redis 的地址。我在 Nacos 里改了配置之后,點發布,然后去看服務日志,發現 Redis 已經連接到新的地址了,完全不用重啟服務,當時就覺得這技術太香了。
5.3 排除某些配置不刷新
有時候,咱們可能不希望某些配置被刷新。比如一些跟服務啟動相關的配置(比如服務端口、數據庫連接池的初始大小),這些配置要是在服務運行中變了,可能會出問題。這時候咱們就可以排除這些配置,不讓它們被 @RefreshScope 刷新。
怎么排除呢?有兩種方法:
方法 1:用 @RefreshScope 的 exclude 屬性
@RefreshScope 注解有一個 exclude 屬性,可以指定哪些配置不刷新。比如咱們的 UserConfig 里,除了 greeting,還有一個 version 配置,咱們不想讓 version 刷新,就可以這么寫:
@Component
@ConfigurationProperties(prefix = "user")
@RefreshScope(exclude = "version") // 排除version配置不刷新
public class UserConfig {
private String greeting;
private String version; // 這個配置不刷新
// Getter和Setter
// ...
}方法 2:在配置文件里配置
咱們也可以在配置文件里配置 spring.cloud.refresh.exclude,指定全局不刷新的配置:
spring:
cloud:
refresh:
exclude: user.version,order.timeout # 多個配置用逗號分隔這樣,user.version 和 order.timeout 這兩個配置就不會被刷新了。
5.4 自定義刷新觸發條件
默認情況下,配置刷新的觸發條件是:調用 refresh 端點,或者配置中心的配置變了。但有時候,咱們可能想自定義觸發條件,比如只有當某個特定的配置變了之后,才觸發刷新;或者只有管理員才能觸發刷新。
要實現自定義觸發條件,咱們可以通過 “監聽 RefreshEvent 事件” 或者 “自定義 RefreshScope” 來實現。這里咱們講一個簡單的方法:監聽 RefreshEvent 事件。
比如咱們想實現:只有當 “user.greeting” 這個配置變了之后,才觸發 UserConfig 的刷新。咱們可以寫一個事件監聽器:
import org.springframework.cloud.context.refresh.event.RefreshEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
publicclass CustomRefreshListener implements ApplicationListener<RefreshEvent> {
@Resource
private RefreshScope refreshScope;
@Override
public void onApplicationEvent(RefreshEvent event) {
// 獲取變化的配置鍵
String changedKey = event.getSource().toString(); // 這里只是示例,實際要根據具體情況獲取
// 判斷是不是user.greeting變了
if ("user.greeting".equals(changedKey)) {
// 只刷新UserConfig這個Bean
refreshScope.refresh("userConfig");
}
}
}這里要說明一下,event.getSource () 返回的是變化的配置信息,不同的配置中心返回的格式可能不一樣,實際開發中需要根據具體的配置中心來解析。比如 Nacos 返回的是一個 Map,包含變化的配置鍵和值。通過這種方式,咱們就能實現 “按需刷新”,只有滿足條件的時候才刷新,這樣能減少不必要的性能消耗。
六、避坑指南:使用 @RefreshScope 容易踩的坑
咱們講了這么多 @RefreshScope 的用法和原理,現在該聊聊大家最關心的 “坑” 了。這些坑都是我和身邊的同事在實際開發中踩過的,希望大家能避開。
6.1 坑 1:忘記加 Getter/Setter 方法,配置綁不上
這個坑我前面已經提過了,但還是要再強調一遍,因為太容易踩了!很多小伙伴在寫 @ConfigurationProperties 的時候,只寫了屬性,沒寫 Getter 和 Setter 方法,結果配置一直綁不上,查了半天都不知道問題在哪。
為什么會這樣呢?因為 @ConfigurationProperties 是通過 Setter 方法來給屬性賦值的。要是沒有 Setter 方法,Spring 就沒辦法把配置文件里的值賦給屬性,自然就綁不上了。
所以,大家在寫 @ConfigurationProperties 的類的時候,一定要記得加 Getter 和 Setter 方法!要是用的是 IDEA,可以按 Alt+Insert 快速生成。
6.2 坑 2:Bean 是 Singleton,卻依賴了 @RefreshScope 的 Bean
這個坑也很常見。比如咱們有一個 Singleton 的 Bean A,依賴了一個 @RefreshScope 的 Bean B。當 Bean B 刷新之后,Bean A 還是會用原來的 Bean B 實例,不會用新的。
為什么呢?因為 Bean A 是 Singleton,在 Spring 容器啟動的時候就創建好了,它依賴的 Bean B 也是在那個時候注入的。當 Bean B 刷新之后,雖然 Bean B 的實例變了,但 Bean A 已經創建好了,不會再重新注入新的 Bean B 實例。
舉個例子:
// Singleton的Bean A
@Component
publicclass BeanA {
// 依賴@RefreshScope的Bean B
@Autowired
private BeanB beanB;
public String getBeanBValue() {
return beanB.getValue();
}
}
// @RefreshScope的Bean B
@Component
@RefreshScope
@ConfigurationProperties(prefix = "bean")
publicclass BeanB {
private String value;
// Getter和Setter
// ...
}當 BeanB 的配置變了之后,BeanB 會刷新,但 BeanA 里的 beanB 還是原來的實例,所以調用 BeanA 的 getBeanBValue () 方法,返回的還是舊的值。怎么解決這個問題呢?有兩種方法:
方法 1:讓 Bean A 也變成 @RefreshScope
把 Bean A 也加上 @RefreshScope 注解,這樣當 Bean B 刷新之后,Bean A 也會被刷新,從而注入新的 Bean B 實例:
@Component
@RefreshScope // 給Bean A加@RefreshScope
public class BeanA {
@Autowired
private BeanB beanB;
public String getBeanBValue() {
return beanB.getValue();
}
}方法 2:用 ObjectProvider 獲取 Bean B
要是不想讓 Bean A 變成 @RefreshScope,咱們可以用 ObjectProvider 來獲取 Bean B。ObjectProvider 是 Spring 4.3 之后提供的一個工具類,用來獲取 Bean 的實例,它會每次都從 Spring 容器里獲取最新的實例:
@Component
public class BeanA {
// 用ObjectProvider獲取Bean B
@Autowired
private ObjectProvider<BeanB> beanBProvider;
public String getBeanBValue() {
// 每次調用都獲取最新的Bean B實例
BeanB beanB = beanBProvider.getIfAvailable();
return beanB.getValue();
}
}這樣,每次調用 getBeanBValue () 方法的時候,都會獲取最新的 Bean B 實例,就算 Bean B 刷新了,也能拿到新的值。我之前就踩過這個坑,當時 Bean A 是 Singleton,依賴了 @RefreshScope 的 Bean B,結果 Bean B 刷新之后,Bean A 一直用舊的實例,查了半天才發現是這個原因。后來改成用 ObjectProvider 之后,問題就解決了。
6.3 坑 3:配置刷新后,靜態變量沒更新
有些小伙伴喜歡在類里用靜態變量來保存配置,比如:
@Component
@RefreshScope
@ConfigurationProperties(prefix = "user")
publicclass UserConfig {
// 靜態變量
privatestaticString greeting;
// Setter方法不是靜態的
publicvoid setGreeting(String greeting) {
UserConfig.greeting = greeting;
}
// Getter方法是靜態的
publicstaticString getGreeting() {
return greeting;
}
}然后在其他地方用 UserConfig.getGreeting () 來獲取配置。但是,當配置刷新之后,你會發現靜態變量 greeting 還是舊的值,沒有更新。為什么呢?因為 @RefreshScope 刷新的時候,會創建新的 UserConfig 實例,然后調用 setGreeting () 方法給實例的 greeting 賦值。但這里的 setGreeting () 方法是給靜態變量賦值,而靜態變量屬于類,不是屬于實例。當創建新的實例的時候,雖然調用了 setGreeting (),但可能因為某些原因(比如靜態變量被其他地方引用,導致值沒有覆蓋),靜態變量的值沒有更新。
而且,在 Spring 里用靜態變量保存配置本身就不是一個好的做法,容易出現線程安全問題和配置不刷新的問題。
所以,大家盡量不要用靜態變量來保存需要刷新的配置。要是實在要用,那就要確保 setGreeting () 方法能正確給靜態變量賦值,并且在刷新的時候能被調用到。
6.4 坑 4:跟 @Cacheable 一起用的時候,緩存不刷新
要是被 @RefreshScope 標記的 Bean 里用了 @Cacheable 注解(緩存),那么當 Bean 刷新之后,緩存可能不會刷新,導致返回的還是舊的數據。
比如咱們有一個 UserService,里面有個方法用了 @Cacheable:
@Service
@RefreshScope
public class UserService {
@Autowired
private UserConfig userConfig;
@Cacheable(value = "userCache", key = "'greeting'")
public String getGreeting() {
return userConfig.getGreeting();
}
}當 UserConfig 刷新之后,調用 getGreeting () 方法,返回的還是舊的緩存值,而不是新的配置。為什么呢?因為 @Cacheable 的緩存是基于方法的返回值,當 Bean 刷新之后,方法的返回值變了,但緩存還在,所以會返回舊的緩存。
怎么解決這個問題呢?有兩種方法:
方法 1:刷新的時候清空緩存
咱們可以在配置刷新的時候,手動清空對應的緩存。比如監聽 RefreshEvent 事件,當事件觸發的時候,清空 userCache 緩存:
@Component
public class CacheClearListener implements ApplicationListener<RefreshEvent> {
@Autowired
private CacheManager cacheManager;
@Override
public void onApplicationEvent(RefreshEvent event) {
// 清空userCache緩存
Cache cache = cacheManager.getCache("userCache");
if (cache != null) {
cache.clear();
}
}
}這樣,當配置刷新之后,緩存也會被清空,下次調用 getGreeting () 方法的時候,就會返回新的配置,并且重新緩存。
方法 2:在 @Cacheable 的 key 里加入配置的版本號
咱們可以給配置加一個版本號,當配置變了之后,版本號也變了,這樣 @Cacheable 的 key 就會變,從而重新緩存。
比如在配置文件里加一個版本號:
user:
greeting: "Hello, 歡迎使用@RefreshScope!"
config-version: 1.0 # 配置版本號然后在 UserConfig 里加 version 屬性:
@Component
@ConfigurationProperties(prefix = "user")
@RefreshScope
public class UserConfig {
private String greeting;
private String configVersion;
// Getter和Setter
// ...
}然后在 @Cacheable 的 key 里加入 version:
@Service
@RefreshScope
public class UserService {
@Autowired
private UserConfig userConfig;
@Cacheable(value = "userCache", key = "'greeting_' + #root.target.userConfig.configVersion")
public String getGreeting() {
return userConfig.getGreeting();
}
}這樣,當配置變了之后,configVersion 也會變,@Cacheable 的 key 就會變成新的,從而重新緩存新的返回值。
6.5 坑 5:SpringBoot 和 SpringCloud 版本不兼容
這個坑也很常見,很多小伙伴引入了 @RefreshScope 的依賴之后,項目啟動不起來,或者啟動之后配置刷新不了,查了半天發現是 SpringBoot 和 SpringCloud 版本不兼容。
比如你用的 SpringBoot 是 2.7.x,卻用了 SpringCloud 2020.0.x 的版本,這就可能出現兼容性問題。因為不同版本的 SpringBoot 和 SpringCloud,里面的 API 可能不一樣,有的類可能被刪除了,有的方法可能被修改了。
怎么避免這個問題呢?很簡單,就是嚴格按照 Spring 官方推薦的版本組合來配置。Spring 官方有一個版本對應表,大家可以去官網查(地址:https://spring.io/projects/spring-cloud#overview)。
我給大家列幾個常用的版本組合,大家可以參考:
SpringBoot 版本 | SpringCloud 版本 | Alibaba Cloud 版本 |
2.6.x | 2021.0.x | 2021.0.1.0 |
2.7.x | 2021.0.x | 2021.0.4.0 |
3.0.x | 2022.0.x | 2022.0.0.0-RC1 |
要是你不知道該用哪個版本,就選上面表格里的組合,基本不會出問題。
七、總結:@RefreshScope 的優缺點和適用場景
咱們聊了這么多,最后來總結一下 @RefreshScope 的優缺點和適用場景,幫助大家更好地在項目中使用它。
7.1 優點
- 不用重啟服務,配置實時生效:這是最大的優點,能大大減少服務中斷的時間,提高服務的可用性。
- 使用簡單,集成方便:只需要加一個注解,引入幾個依賴,就能實現動態刷新,學習成本低。
- 支持多種配置來源:能跟 Nacos、Apollo、Config Server 等主流配置中心無縫集成,也支持本地配置文件。
- 靈活性高:支持排除某些配置不刷新、自定義觸發條件等,能滿足各種復雜的需求。
7.2 缺點
- 有一定的性能消耗:每次刷新的時候,都會銷毀舊的 Bean 實例,創建新的實例。要是被 @RefreshScope 標記的 Bean 很多,或者刷新頻率很高,會有一定的性能消耗。
- 可能出現數據不一致:在刷新的過程中,要是有線程正在使用舊的 Bean 實例,而另一個線程已經使用新的 Bean 實例,可能會出現數據不一致的問題(不過這種情況很少見,只要不是高并發場景,基本不用考慮)。
- 不支持所有配置:有些跟服務啟動相關的配置(比如服務端口、JVM 參數),沒辦法用 @RefreshScope 刷新,因為這些配置在服務啟動之后就固定了,改了也沒用。
7.3 適用場景
- 經常需要修改的配置:比如短信模板、支付回調地址、限流閾值、日志級別等,這些配置可能會經常變,用 @RefreshScope 能避免重啟服務。
- 高可用要求高的服務:比如電商的訂單服務、支付服務,這些服務不能隨便重啟,用 @RefreshScope 能減少服務中斷的風險。
- 分布式系統:在分布式系統中,服務很多,要是每個服務改配置都要重啟,那運維成本太高了。用 @RefreshScope 配合配置中心,能實現配置的統一管理和實時刷新,大大降低運維成本。
7.4 不適用場景
- 跟服務啟動相關的配置:比如服務端口、數據庫連接池的初始大小、JVM 參數等,這些配置在服務啟動之后就生效了,改了也沒辦法實時生效,不用 @RefreshScope。
- 刷新頻率很高的場景:比如每秒都要改配置,這種情況用 @RefreshScope 會有很大的性能消耗,不推薦。
- 對數據一致性要求極高的場景:比如金融交易系統,在配置刷新的瞬間,可能會出現數據不一致的問題,雖然概率很低,但還是要謹慎使用。
到這里,關于 SpringBoot + @RefreshScope 的內容就全部講完了。從基礎用法到深入原理,再到進階玩法和避坑指南,應該能幫大家把這個知識點吃透了。
其實 @RefreshScope 不算什么高深的技術,但它在實際開發中非常實用,能幫咱們解決很多痛點。我希望大家學完之后,能在項目中用上它,不用再因為改配置而重啟服務了。

































