1.前言
前文鏈接:Spring Cloud 學習——7. Spring Cloud Config
前一篇文章我們學習了通過 Spring Cloud Config + git 實現分布式系統的統一配置管理。但是在實際項目中,我們只是實現配置往往是不夠的,我們經常會遇到需要在項目運行時修改配置的需求。接下來我們就學習一下,如何在 Spring Cloud Config 環境下動態地(不重啟服務)修改配置。
我們分三步實現:
第一步: 手動動態刷新單個服務實例的配置;
第二步:手動動態刷新整個系統所有服務實例的配置;
第三步:自動動態刷新整個系統所有服務實例的配置;
2.第一步,實現手動動態刷新單個服務實例的配置
2.1.在 config-client 服務模塊添加依賴(版本自選)

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>2.2.4.RELEASE</version> </dependency>
actuator 是一個庫,通過暴露端點的方式,它可以幫助我們監控和管理 spring boot 應用,追蹤、收集、審計、統計應用的運行情況。
2.2.配置暴露端點
在 bootstrap 配置文件中,添加配置 management.endpoints.web.exposure.include=* ,這里我們填 * ,即暴露所有端點。
2.3.聲明需要刷新配置的bean
對需要動態刷新配置的 bean 類添加注解 @RefreshScope ,該注解的作用是告訴 spring 容器,當程序運行過程中重新向 config-server 拉取配置時,這個 bean 需要重新加載,並將配置屬性字段使用對應的配置項最新獲取的配置值注入。如:

@Component @RefreshScope public class CommonConfig { @Value("${username}") private String username; @Value("${nickname}") private String nickname; @Value("${version}") private String version;
2.4.測試驗證
2.4.1.首先編寫一個接口,將配置的 version 值返回。代碼如下:

@RestController @RequestMapping("/") public class HomeController { @Autowired CommonConfig commonConfig; @GetMapping("/") public String index(){ return "version = " + commonConfig.getVersion(); }
並同時啟動該服務的兩個實例,實例的服務端口分別是:7200 和 7201。
2.4.2.執行請求接口:http://localhost:7200/,查看當前的 version 值,結果:
2.4.3.接下來我們修改 git 上配置的值為 23
2.4.4.重新執行上述接口請求,發現結果沒變。因為我們現在還沒有執行刷新的動作。
2.4.5.現在來執行一下刷新,方法是請求接口:http://localhost:7200/actuator/refresh。注意該接口需要指定 post 請求,Content-Type:application/json,參數可以為空。該接口返回是一個空數組。
查看一下控制台打印,發現在執行刷新接口的時候,服務去向 config-server 重新獲取配置了,7300 是我 config-server 的端口:
2.4.6.現在再執行上述查看配置的接口,結果:
可以看到,7200 端口的實例上配置已經更新了。那么 7201 端口的實例上,配置是否更新了呢,執行一下:http://localhost:7201/,結果是並沒有:
所以到目前為止,我們實現了第一步:手動動態刷新單個服務實例的配置。接下來我們進行第二步。
3.第二步:實現手動動態刷新整個系統所有服務實例的配置
回顧上一步,我們發現其實就是通過 post 服務暴露的 /actuator/refresh 接口,觸發了服務去 config-server 配置中心重新拉取配置了。但是在一個分布式系統中,可能存在非常多的服務實例,如果每一台實例都要手動去 post 一下,那么運維成本是很高的,而且還會存在一段比較長的時間內同一個服務的多個實例的配置值有新的有舊的這種情況。那么我們能不能實現只 post 一次,就同時刷新了所有實例的配置呢?
當然是可以的。要實現這一點,需要借助 Spring Cloud Bus + 消息中間件。Spring Cloud Bus 目前支持的中間件有 kafka 和 rabbitMQ,本例中,消息中間件使用 kafka。
3.1.搭建 kafka
請參考:centos 安裝 kafka
3.2.添加依賴
在 config server 和 config client 都添加依賴:

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-bus-kafka --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-kafka</artifactId> <version>2.2.0.RELEASE</version> </dependency>
3.3.配置 config server

spring.kafka.bootstrap-servers=host:port spring.cloud.bus.refresh.enabled=true management.endpoints.web.exposure.include=*
其中:
spring.kafka.bootstrap-servers 指定 kafka 的服務地址,多個使用英文逗號分隔;
spring.cloud.bus.refresh.enabled=true 開啟 bus 刷新功能;
3.4.配置 config client
spring.kafka.bootstrap-servers=47.107.76.147:9092
對比 config server 的配置,我們發現 config 和 server 都需要配置 kafka 服務地址,因為到時候刷新配置的廣播是通過 kafka 發出和接收的。而 client 比 server 少了一個開啟的配置,因為我們是將 server 作為最初接收 post 的節點,然后它通過 kafka 將消息廣播給所有 client ,這些 client 接收到廣播之后,再去 server 上面拉取配置。就是這么一個流程。
3.5.驗證測試
3.5.1.我們先重啟服務,訪問一下:http://localhost:7201/,結果:
然后訪問:http://localhost:7200/,結果也是 23:
3.5.2.現在把 git 上的配置改成 24
3.5.2.然后執行刷新,現在我們不再 post 單個實例的刷新接口了,而是 post 配置中心(config server)的 bus 刷新接口:
接口返回異常了,我們看一下 狀態碼:
204代表接口請求是成功的,只是沒有數據返回。那我們再看一下兩個實例的 控制台打印,是都去 config server 拉取配置了的:
3.5.3.驗證結果是不是刷新了,分別請求兩個實例的查看接口:
可以看到,兩個配置的實例都已經刷新了。
雖然這里只是用到了同一個服務的兩個實例,但是該方法是針對所有服務有效的,只要這些服務都配置了同一個配置中心並且做相同的配置即可。其實這很容易理解,因為對於配置中心來講:不管你是一個什么服務(用戶服務、訂單服務,等等),你在我眼里就是一個配置客戶端(config client)而已,其它的我並不關心,也就是說只要你把我配置為你的 config server,我就可以通過 bus 通知你刷新配置。
到目前為止,我們已經實現了第二步,手動動態刷新整個系統所有服務實例的配置。
4.第三步,實現自動動態刷新整個系統所有服務實例的配置
在上一步,我們已經實現了一次刷新整個系統的配置,現在我們只是需要加一個自動的功能。也就是說,只需要在配置修改的時候,觸發一個向 config server 的 /actuator/bus-refresh 接口的請求就行了。
剛好,git 服務器有一個 webhook,就支持在倉庫有動作的時候,發送一個 http post 請求。配置方式(gitee 例):
在我的項目中,我是將項目部署在阿里雲上,並且沒有對外網暴露我的 config server 的,而是通過 netflix-zuul 轉發到 config server。但是在轉發的過程中,git 這邊收到的返回結果是異常的,看日志好像是參數解析的問題(應該是因為 zuul 對請求的頭信息和參數做了一些什么事情,還沒有細究 zuul),但是我又不清楚 /actuator/bus-refresh 是怎么去解析參數的,而且我們也不能更改 git 提交的參數內容。那咋辦呢,那就自己定義一個接口,讓 webhook 服務 post 到這個自定義的接口(這個自定義接口可以不在乎參數,所以不用解析),然后在這個接口里面使用我自己定義的參數 post 一下 /actuator/bus-refresh ,這樣就可以避免雙方參數不一致的問題了。代碼如下:

@RestController @RequestMapping("/") public class HomeController { @GetMapping("/") public String home(){ return "success"; } @PostMapping("/refresh/notify") public String refresh(HttpServletRequest request){ String jsonStr = "{\"ping\":\"pong\"}"; CloseableHttpResponse response = null; try(CloseableHttpClient client = HttpClientBuilder.create().build()){ HttpUriRequest httpRequest = RequestBuilder.post(request.getRequestURL().toString().replaceAll(request.getServletPath(), "/actuator/bus-refresh")) .setHeader("Content-Type", "application/json") .setEntity(new StringEntity(jsonStr)) .build(); response = client.execute(httpRequest); }catch (Exception e){ System.out.println("post /actuator/bus-refresh 出錯"); }finally { if (response != null){ try { response.close(); }catch (IOException ioe){ System.out.println("關閉 response 異常"); } } } return jsonStr; } }
查看一下 webhook 的返回結果:
成功返回了結果。同時重新獲取一下配置,是刷新了的,就不截圖了。
當然,也可以不自定義接口,而是:
1.解決原始 webhook 向 zuul 請求報錯的問題;
2.或者直接將 config-server 的 /actuator/bus-refresh 暴露到外網;
5.完