Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(二十三):配置中心(Config、Bus)
線上演示
使用者名稱:admin 密碼:admin
技術背景
如今微服務架構盛行,在分散式系統中,專案日益龐大,子專案日益增多,每個專案都散落著各種配置檔案,且隨著服務的增加而不斷增多。此時,往往某一個基礎服務資訊變更,都會導致一系列服務的更新和重啟,運維也是苦不堪言,而且還很容易出錯。於是,配置中心便由此應運而生了。
目前市面上開源的配置中心有很多,像Spring家族的Spring Cloud Config, Apache的Apache Commons Configuration,淘寶的diamond, 百度的disconf, 360的QConf等等,都是為了解決這類問題。當下Spring體系大行其道,我們當然也優先選擇Spring Cloud Config了。
Spring Cloud Config
Spring Cloud Config 是一套為分散式系統中的基礎設施和微服務應用提供集中化配置的管理方案,它分為服務端與客戶端兩個部分。服務端也稱為分散式配置中心,它是一個獨立的微服務應用,用來連線配置倉庫併為客戶端提供獲取配置資訊。客戶端則是微服務架構中的各個微服務應用或基礎設施,它們通過指定的配置中心來管理服務相關的配置內容,並在啟動的時候從配置中心獲取和載入配置資訊。
Spring Cloud Config對服務端和客戶端中的環境變數和屬性配置 實現了抽象對映,所以它除了適用於 Spring 應用,也是可以在任何其他語言應用中使用的。Spring Cloud Config 實現的配置中心預設採用 Git 來儲存配置資訊,所以使用 Spring Cloud Config 構建的配置伺服器,天然就支援對微服務應用配置資訊的版本管理,並且可以通過 Git 客戶端工具非常方便的管理和訪問配置內容。當然它也提供了對其他儲存方式的支援,比如:SVN 倉庫、本地化檔案系統等。
實現案例
準備配置檔案
首先在GIT下,新建config-repo目錄,用來存放配置檔案,如下圖所示,分別模擬了三個環境的配置檔案。
分別編輯三個檔案,配置 comsumer.hello 屬性的值為 comsumer.hello=hello, this is xx configurations.
服務端實現
新建工程
新建 kitty-conifg 工程,作為配置中心的服務端,負責把GIT倉庫的配置檔案釋出為RESTFul介面。
新增依賴
除了Spring Cloud依賴之外,新增配置中心依賴包。
pom.xml
<!--spring config--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies>
啟動類
啟動類添加註解 @EnableConfigServer,開啟配置服務支援。
KittyConfigApplication.java
package com.louis.kitty.config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.config.server.EnableConfigServer; @EnableConfigServer @EnableDiscoveryClient @SpringBootApplication public class KittyConfigApplication { public static void main(String[] args) { SpringApplication.run(KittyConfigApplication.class, args); } }
配置檔案
修改配置檔案,新增如下內容。如果是私有倉庫需要填寫使用者名稱密碼,如果是公開倉庫,可以不配置密碼。
application.yml
server: port: 8020 spring: application: name: kitty-config cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 註冊到consul的服務名稱 config: label: master # git倉庫分支 server: git: uri: https://gitee.com/liuge1988/kitty.git # 配置git倉庫的地址 search-paths: config-repo # git倉庫地址下的相對地址,可以配置多個,用,分割。 username: username # git倉庫的賬號 password: password # git倉庫的密碼
Spring Cloud Config也提供本地儲存配置的方式,只需設定屬性spring.profiles.active=native
,Config Server會預設從應用的src/main/resource
目錄下檢索配置檔案。另外也可以通過spring.cloud.config.server.native.searchLocations=file:D:/properties/
屬性來指定配置檔案的位置。雖然Spring Cloud Config提供了這樣的功能,但是為了更好的支援內容管理和版本控制,還是比較推薦使用GIT的方式。
測試效果
{ "name": "kitty-consumer", "profiles": ["dev"], "label": null, "version": "1320259308dfdf438f5963f95cbce9e0a76997b7", "state": null, "propertySources": [{ "name": "https://gitee.com/liuge1988/kitty.git/config-repo/kitty-consumer-dev.properties", "source": { "consumer.hello": "hello, this is dev configurations. " } }] }
{ "name": "kitty-consumer", "profiles": ["pro"], "label": null, "version": "1320259308dfdf438f5963f95cbce9e0a76997b7", "state": null, "propertySources": [{ "name": "https://gitee.com/liuge1988/kitty.git/config-repo/kitty-consumer-pro.properties", "source": { "consumer.hello": "hello, this is pro configurations. " } }] }
上述的返回的資訊包含了配置檔案的位置、版本、配置檔案的名稱以及配置檔案中的具體內容,說明server端已經成功獲取了git倉庫的配置資訊。
修改一下dev配置檔案內容如下(末尾加了一個 2):
發現讀取的是修改後提交的資訊,說明服務端會自動讀取最新提交的資料。
倉庫中的配置檔案會被轉換成相應的WEB介面,訪問可以參照以下的規則:
- /{application}/{profile}[/{label}]
- /{application}-{profile}.yml
- /{label}/{application}-{profile}.yml
- /{application}-{profile}.properties
- /{label}/{application}-{profile}.properties
以kitty-consumer-dev.properties為例子,它的application是kitty-consumer,profile是dev。client會根據填寫的引數來選擇讀取對應的配置。
客戶端實現
新增依賴
開啟kitty-consumer工程,新增相關依賴。
pom.xml
<!-- spring-cloud-config --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
配置檔案
新增一個bootstrap.yml配置檔案,新增配置中心,並把註冊中心的配置移到這裡,因為在通過配置中心查詢配置時需要通過註冊中心的發現服務。
bootstrap.yml
spring: cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 註冊到consul的服務名稱 config: discovery: enabled: true # 開啟服務發現 serviceId: kitty-config # 配置中心服務名稱 name: kitty-consumer # 對應{application}部分 profile: dev # 對應{profile}部分 label: master # 對應git的分支,如果配置中心使用的是本地儲存,則該引數無用
配置說明:
- spring.cloud.config.uri:配置中心的具體地址
- spring.cloud.config.name:對應{application}部分
- spring.cloud.config.profile:對應{profile}部分
- spring.cloud.config.label:對應git的分支。如果配置中心使用的是本地儲存,則該引數無用
- spring.cloud.config.discovery.service-id:指定配置中心的service-id,便於擴充套件為高可用配置叢集。
特別注意:
上面這些與spring cloud相關的屬性必須配置在bootstrap.yml中,這樣config部分內容才能被正確載入。
因為config的相關配置會先於application.yml,而bootstrap.yml的載入也是先於application.yml檔案的。
application.yml
server: port: 8005 spring: application: name: kitty-consumer boot: admin: client: url: "http://localhost:8000" zipkin: base-url: http://localhost:9411/ sleuth: sampler: probability: 1 #樣本採集量,預設為0.1,為了測試這裡修改為1,正式環境一般使用預設值 # 開放健康檢查介面 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS #開啟熔斷器 feign: hystrix: enabled: true
控制器
新增一個 SpringConfigController 控制器, 添加註解 @Value("${comsumer.hello}"),宣告hello屬性從配置檔案讀取。
SpringConfigController.java
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController class SpringConfigController { @Value("${comsumer.hello}") private String hello; @RequestMapping("/hello") public String from() { return this.hello; } }
測試效果
說明客戶端已經成功從服務端讀取了配置資訊。
現在手動修改一下倉庫配置檔案的內容,移除末尾數字 2,修改完成並提交。
我們發現返回結果並沒有讀取最新提交的內容,這是因為Spring Boot專案只有在啟動的時候才會獲取配置檔案的內容,雖然GIT配置資訊被修改了,但是客戶端並沒有重新去獲取,所以導致讀取的資訊仍然是舊配置。那麼該如何去解決這個問題呢?這就是我們下一章要講的 Spring Cloud Bus。
Refresh機制
我們在上面講到,Spring Boot程式只在啟動的時候載入配置檔案資訊,這樣在GIT倉庫配置修改之後,雖然配置中心伺服器能夠讀取最新的提交資訊,但是配置中心客戶端卻不會重新讀取,以至於不能及時的讀取更新後的配置資訊。這個時候就需要一種通知重新整理機制來支援了。
refresh機制是Spring Cloud Config提供的一種重新整理機制,它允許客戶端通過POST方法觸發各自的/refresh,只要依賴spring-boot-starter-actuator包就擁有了/refresh的功能,下面我們為我們的客戶端加上重新整理功能,以支援更新配置的讀取。
新增依賴
我們的 kitty-consumer 在之前已經新增過actuator依賴,所以這裡就不用添加了,如果之前沒有新增需要加上。actuator是健康檢查依賴包,依賴包裡攜帶了 /refresh 的功能。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
在使用配置屬性的型別加上 @RefreshScope 註解,這樣在客戶端執行 /refresh 的時候就會重新整理此類下面的配置屬性了。
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RefreshScope @RestController class SpringConfigController { @Value("${consumer.hello}") private String hello; @RequestMapping("/hello") public String from() { return this.hello; } }
修改配置
健康檢查介面開放需要在配置檔案新增以下內容,開放refresh的相關介面,因為這個我們在之前也配置過了,所以也不需添加了。
management:
endpoints:
web:
exposure:
include: "*"
通過上面的介面開放配置,以後以post請求的方式訪問 http://localhost:8005/actuator/refresh 時,就會更新修改後的配置檔案了。
特別注意:
這裡存在著版本大坑,1.x跟2.x的配置不太一樣,我們用的是2.0+版本,務必注意。
1.安全配置變更
新版本
management.endpoints.web.exposure.include="*"
老版本
management.security.enabled=false
2.訪問地址變更
新版本
http://localhost:8005/actuator/refresh
老版本
http://localhost:8005/refresh
這裡還是解釋一下上面這個配置起到了什麼具體作用,其實actuator是一個健康檢查包,它提供了一些健康檢查資料介面,refresh功能也是其中的一個介面,但是為了安全起見,它預設只開放了health和info介面(啟動資訊會包含如下圖所示資訊),而上面的配置就是設定要開放哪些介面, 我們設定成 “*”,是開放所有介面。你也可以指定開發幾個,比如: health,info,refresh,而這裡因為我們需要用的refresh功能,所以需要把refresh介面開放出來。
設定成 “*” 後,啟動資訊會包含以下資訊,而這個叫refresh的post方法,就是我們需要的,上面說的介面地址變更從這裡也可以看得出來。
測試效果
修改倉庫配置內容,末尾加個數字 5,如下圖所示。
注意:先讓你的Chrome支援跨域。設定方法:在快捷方式的target後加上 --disable-web-security --user-data-dir,重啟即可。
檢視返回結果,重新整理之後已經可以獲取最新提交的配置內容,但是每次都需要手動重新整理客戶端還是很麻煩,如果客戶端數量一多就簡直難以忍受了,有沒有什麼比較好的辦法來解決這個問題呢,那是當然的,答案就是:Spring Cloud Bus。
Spring Cloud Bus
Spring Cloud Bus,被大家稱為訊息匯流排,它通過輕量級的訊息代理來連線各個分佈的節點,可以利用像訊息佇列的廣播機制在分散式系統中進行訊息傳播,通過訊息匯流排可以實現很多業務功能,其中對於配置中心客戶端重新整理,就是一個非常典型的使用場景。
下面這張圖可以很好的解釋訊息匯流排的作用流程(圖片描述來源:純潔的微笑:配置中心博文)。
Spring Cloud Bus 進行配置更新步驟如下:
1、提交程式碼觸發post請求給/actuator/bus-refresh
2、server端接收到請求併發送給Spring Cloud Bus
3、Spring Cloud bus接到訊息並通知給其它客戶端
4、其它客戶端接收到通知,請求Server端獲取最新配置
5、全部客戶端均獲取到最新的配置
安裝RabbitMQ
因為我們需要用到訊息佇列,我們這裡選擇RabbitMQ,使用Docker進行安裝。
拉取映象
執行以下命令,拉取映象。
docker pull rabbitmq:management
完成之後執行以下命令檢視下載映象。
docker images
建立容器
執行以下命令,建立docker容器。
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
啟動成功之後,可以執行以下命令檢視啟動容器。
docker ps
登入介面
容器啟動之後就可以訪問web管理介面了,訪問 http://宿主機IP:15672。
系統提供了預設賬號。 使用者名稱:guest 密碼: guest
管理介面
客戶端實現
新增依賴
開啟客戶端 kitty-consumer,新增訊息匯流排相關依賴。
pom.xml
<!-- bus-amqp --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改配置
修改配置,新增RebbitMq的相關配置,這樣客戶端程式碼就改造完成了。
bootstrap.yml
spring: cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 註冊到consul的服務名稱 config: discovery: enabled: true # 開啟服務發現 serviceId: kitty-config # 配置中心服務名稱 name: kitty-consumer # 對應{application}部分 profile: dev # 對應{profile}部分 label: master # 對應git的分支,如果配置中心使用的是本地儲存,則該引數無用 rabbitmq: host: localhost port: 5672 username: guest password: guest
服務端實現
新增依賴
修改 kitty-conifg,新增相關依賴。
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改配置,新增RabbitMq的和介面開放相關配置,這樣服務端程式碼也改造完成了。
application.yml
server: port: 8020 spring: application: name: kitty-config cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 註冊到consul的服務名稱 config: label: master # git倉庫分支 server: git: uri: https://gitee.com/liuge1988/kitty.git # 配置git倉庫的地址 search-paths: config-repo # git倉庫地址下的相對地址,可以配置多個,用,分割。 username: username # git倉庫的賬號 password: password # git倉庫的密碼 rabbitmq: host: localhost port: 5672 username: guest password: guest management: endpoints: web: exposure: include: "*"
測試效果
1.啟動服務端,成功整合訊息匯流排後,啟動資訊中可以看到如下圖中的資訊。
2.啟動客戶端,發現居然報錯了,網上也找不到相關資料,也沒見其他人提過相關問題。猜測是網上教程多是使用Euraka,而這裡用的時Consul,瞎鼓搗了好久,反正是不想換回Euraka,2.0停止開發訊息出來以後,將來還不定什麼情況,只能硬著頭皮解決了。
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'configServerRetryInterceptor' available at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:685) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1210) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:291) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.getDelegate(AnnotationAwareRetryOperationsInterceptor.java:180) ~[spring-retry-1.2.2.RELEASE.jar:na] at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:151) ~[spring-retry-1.2.2.RELEASE.jar:na] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.cloud.config.client.ConfigServerInstanceProvider$$EnhancerBySpringCGLIB$$dd44720b.getConfigServerInstances(<generated>) ~[spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE] at org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration.refresh(DiscoveryClientConfigServiceBootstrapConfiguration.java:84) [spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE] at org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration.startup(DiscoveryClientConfigServiceBootstrapConfiguration.java:69) [spring-cloud-config-client-2.0.0.RELEASE.jar:2.0.0.RELEASE] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
然後就跟蹤程式碼,發現是在下圖中的位置找不到相應的Bean,那麼答案就比較明顯了,要麼是程式有BUG,不過可能性不大,那應該是就是缺包了,在缺失的包裡有這個Bean。但是這個Bean是在哪個包?排查了半天也沒找到,網上也沒有想過資料,對比了一下網上訊息匯流排的配置,依賴也沒有少加什麼。
沒有辦法,最後只能自己上手了,不就是在重新整理的時候缺少一個攔截器嗎,自己給他弄一個試試唄。
使用就加了一個配置類,並在resources下新建了META-INF目錄和一個spring。factories檔案。
RetryConfiguration.java
package com.louis.kitty.consumer; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.retry.interceptor.RetryInterceptorBuilder; import org.springframework.retry.interceptor.RetryOperationsInterceptor; public class RetryConfiguration { @Bean @ConditionalOnMissingBean(name = "configServerRetryInterceptor") public RetryOperationsInterceptor configServerRetryInterceptor() { return RetryInterceptorBuilder.stateless().backOffOptions(1000, 1.2, 5000).maxAttempts(10).build(); } }
spring.factories
org.springframework.cloud.bootstrap.BootstrapConfiguration=com.louis.kitty.consumer.RetryConfiguration
在這裡指定新建的攔截器,這樣系統初始化時會載入這個Bean。
然後重啟啟動,果然沒有報錯了,還是先別高興,看看能不能用先。
5.修改倉庫配置檔案,把數字5改成15,修改完成提交。
再次訪問發現還是舊資訊。
注意這次是向註冊中心服務端傳送請求,傳送成功之後服務端會通過訊息匯流排通知所有的客戶端進行重新整理。
另外開啟訊息匯流排後的請求地址是 /actuator/bus-refresh,不再是refresh了。
我們愉快的發現客戶端已經能夠通過訊息匯流排獲取最新配置了。