原文地址:Spring Cloud 入門 之 Config 篇(六)
博客地址:http://www.extlight.comjava
隨着業務的擴展,爲了方便開發和維護項目,咱們一般會將大項目拆分紅多個小項目作成微服務,每一個微服務都會有各自配置文件,管理和修改文件起來也會變得繁瑣。並且,當咱們須要修改正在運行的項目的配置時,一般須要重啓項目後配置才能生效。git
上述的問題將是本篇須要解決的問題。github
Spring Cloud Config 用於爲分佈式系統中的基礎設施和微服務應用提供集中化的外部配置支持,它分爲服務端和客戶端兩部分。服務端(config server)也稱爲分佈式配置中心,是一個獨立的微服務應用,用來鏈接配置倉庫併爲客戶端提供獲取配置信息,加密/解密信息等訪問接口。而客戶端(config client)則是微服務架構中各微服務應用或基礎設施,經過指定的配置中心來管理應用資源與業務相關的配置內容,並在啓動的時候從配置中心獲取和加載配置信息。web
如上圖,當 Config Client 首次啓動時會向 Config Server 獲取配置信息,Config Server 接收到請求再從遠程私有倉庫獲取配置(鏈接不上項目會報錯),並保存到本地倉庫中。spring
當 Config Client 再次啓動時會向 Config Server 獲取配置信息,Config Server 仍是會先從遠程私有倉庫拉去數據。若是網絡問題或認證問題致使沒法鏈接遠程私有庫,Config Server 纔會從本地倉庫獲取配置信息返回給 Config Client。json
本次實戰基於 Eureka 篇的項目進行擴展演練。不清楚的讀者請先轉移至 《Spring Cloud 入門 之 Eureka 篇(一)》 進行瀏覽。bootstrap
咱們使用配置中心來維護 order-server 的配置數據(application.yml)。api
測試場景:因爲配置中心服務自己也是一個微服務,所以咱們須要將配置中心註冊到 Eureka 上,當 order-server 啓動時先向 Eureka 獲取配置中心的訪問地址,而後從配置中心獲取相應的配置信息進行正常啓動。瀏覽器
本篇實戰用到的項目列表:網絡
服務實例 | 端口 | 描述 |
---|---|---|
eureka-server | 9000 | 註冊中心(Eureka 服務端) |
config-server | 10000 | 配置中心(Eureka 客戶端、Config 服務端) |
order-server | 8100 | 訂單服務(Eureka 客戶端、Config 客戶端) |
在 GitHub 上新建一個私有倉庫,名爲 spring-cloud-config。
咱們將 order-server 項目的配置文件放到改倉庫中,以下圖:
新建 2 個 yml 文件,內容爲:
server: port: 8100 eureka: instance: instance-id: order-api-8100 prefer-ip-address: true # 訪問路徑能夠顯示 IP env: dev
ORDER-dev.yml 和 ORDER-test.yml 不一樣之處在於 env 的值,其中一個是 dev ,另外一個是 test。
新建一個 spring boot 項目,名爲 config-server(任意名字)。
1) 添加依賴:
<dependencies> <!-- eureka 客戶端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- config 服務端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies>
2) application.yml
server: port: 10000 spring: application: name: CONFIG cloud: config: server: git: uri: https://github.com/moonlightL/spring-cloud-config.git username: moonlightL password: xxx basedir: d:/data # 本地庫目錄 eureka: instance: instance-id: config-api client: service-url: defaultZone: http://localhost:9000/eureka/ # 註冊中心訪問地址
3) 啓動類添加 @EnableConfigServer:
@EnableConfigServer @EnableEurekaClient @SpringBootApplication public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } }
啓動成功後,咱們打開瀏覽器訪問 http://localhost:10000/order-dev.yml 和 http://localhost:10000/order-test.yml,結果以下圖:
config-server 服務成功拉去遠程私有倉庫的配置數據。
其中,訪問規則以下:
<IP:PORT>/{name}-{profiles}.yml <IP:PORT>/{label}/{name}-{profiles}.yml
name:文件名,可看成服務名稱
profiles: 環境,如:dev,test,pro
lable: 分支,指定訪問某分支下的配置文件,默認拉去 master 分支。
在 order-server 項目中。
1) 添加依賴:
<!-- config 客戶端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency>
2) 刪除 application.yml,並新建 bootstrap.yml,保存以下內容:
spring: application: name: ORDER cloud: config: discovery: enabled: true service-id: CONFIG # config-server 在註冊中心的名稱 profile: dev # 指定配置文件的環境 eureka: client: service-url: defaultZone: http://localhost:9000/eureka/ # 註冊中心訪問地址
配置中,經過 spring.cloud.config.discovery.service-id 肯定配置中心,再經過 spring.application.name 的值拼接 spring.cloud.config.profile 的值,從而肯定須要拉去從配置中心獲取的配置文件。(如:ORDER-dev)
注意:必須保留 eureka 註冊中心的配置,不然 order-server 沒法鏈接註冊中心,也就沒法獲取配置中心(config-server)的訪問信息。
3) 測試
新建一個測試類:
@RestController @RequestMapping("/test") public class TestController { @Value("${env}") private String env; // 從配置中心獲取 @RequestMapping("/getConfigInfo") public String getConfigInfo() { return env; } }
打開瀏覽器訪問 http://localhost:8100/test/getConfigInfo,結果以下圖:
成功獲取 config-server 從遠程私有倉庫拉去的數據,因爲在 bootstrap.yml 中配置了 spring.cloud.config.profile=dev,所以拉取到的數據就是 ORDER-dev.yml 中的數據。
引伸問題:
當咱們修改遠程私有倉庫的配置文件時,Config Server 如何知道是否該從新獲取遠程倉庫數據呢?
如今已知惟一的解決方式就是重啓 Config Client 項目,在項目啓動時會請求 Config Server 從新拉去遠程私有倉庫數據。可是,若是是在生產環境下隨便重啓項目一定會影響系統的正常運行,那有沒有更好的方式解決上述的問題呢?請讀者繼續閱讀下文。
Spring Cloud Bus 是 Spring Cloud 家族中的一個子項目,用於實現微服務之間的通訊。它整合 Java 的事件處理機制和消息中間件消息的發送和接受,主要由發送端、接收端和事件組成。針對不一樣的業務需求,能夠設置不一樣的事件,發送端發送事件,接收端接受相應的事件,並進行相應的處理。
在 config-server 項目中:
1) 添加依賴:
<!-- bus --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
2) 修改 application.yml,添加以下配置:
server: port: 10000 spring: application: name: CONFIG cloud: config: server: git: uri: https://github.com/moonlightL/spring-cloud-config.git username: moonlightL password: shijiemori960 rabbitmq: host: 192.168.2.13 port: 5672 username: light password: light eureka: instance: instance-id: config-api client: service-url: defaultZone: http://localhost:9000/eureka/ # 註冊中心訪問地址 management: endpoints: web: exposure: include: "*" # 暴露接口
添加了 rabbitmq 配置和 management 的配置。
在 order-server 項目中:
1) 添加依賴:
<!-- bus --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
2) 修改 bootstrap.yml:
spring: application: name: ORDER cloud: config: discovery: enabled: true service-id: CONFIG # config-server 在註冊中心的名稱 profile: dev # 指定配置文件的環境 rabbitmq: host: 192.168.2.13 port: 5672 username: light password: light eureka: client: service-url: defaultZone: http://localhost:9000/eureka/ # 註冊中心訪問地址
添加 rabbitmq 配置。
3) 獲取數據的類上添加 @RefreshScope 註解:
@RestController @RequestMapping("/test") @RefreshScope public class TestController { @Value("${env}") private String env; // 從配置中心獲取 @RequestMapping("/getConfigInfo") public String getConfigInfo() { return env; } }
整合 Bus 後的原理圖以下:
當咱們修改遠程私有倉庫配置信息後,須要向 Config Server 發起 actuator/bus-refresh 請求。而後, Config Server 會通知消息總線 Bus,以後 Bus 接到消息並通知給其它鏈接到總線的 Config Client。最後,Config Client 接收到通知請求 Config Server 端從新訪問遠程私有倉庫拉去最新數據。
4) 測試:
修改遠程私有倉庫配置文件,使用 Postman 發起 POST 請求 http://localhost:10000/actuator/bus-refresh,最終配置中心從新拉去數據,最後再訪問 order-server http://localhost:8100/test/getConfigInfo 獲取最新數據,運行結果以下圖:
如上圖,咱們實現了在不重啓項目的狀況下,獲取變動數據的功能。
引伸問題:
每次更新私有倉庫中的配置文件都須要手動請求 actuator/bus-refresh,仍是不夠自動化。
下邊咱們來解決該問題。
遠程私有倉庫的提供 WebHook 配置,咱們將 actuator/bus-refresh 配置上去,當遠程私有倉庫中的配置信息發生變更時,就會自動調用該接口最終實現自動刷新目的。
登陸 GitHub,點擊 GitHub 的 WebHook 菜單,右側面板中 Payload URL 填寫 <配置中心 url> /actuator/bus-refresh, Content-type 選擇 applicaton/json,保存便可。
因爲筆者是本地測試,沒有外網域名,所以藉助 https://natapp.cn 作外網映射(操做簡單,詳情看官網教程),如下是筆者的外網信息:
設置 WebHook 操做以下圖:
預期效果:當咱們修改 GitHub 上私有倉庫的配置數據後,咱們再訪問 http://localhost:8100/test/getConfigInfo 應該展現最新的數據。
可是結果失敗了。
緣由:
回到 config-server 控制檯查看日誌發現報錯了:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token at [Source: (PushbackInputStream); line: 1, column: 68] (through reference chain: java.util.LinkedHashMap["hook"])]
這是由於,GitHub 在調用 <配置中心 url> /actuator/bus-refresh 時,往請求體添加了 payload 數據,但它不是一個標準的 JSON 數據。所以,config-server 在接收 GitHub 發送的請求獲取,從請求體數據作轉換時就報錯了。
解決方案:
在 config-server 項目中,新建一個過濾器,用於過濾 actuator/bus-refresh 請求,將其請求體置空:
@Component public class WebHookFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String url = new String(httpServletRequest.getRequestURI()); // 只過濾 /actuator/bus-refresh 請求 if (!url.endsWith("/actuator/bus-refresh")) { chain.doFilter(request, response); return; } // 使用 HttpServletRequest 包裝原始請求達到修改 post 請求中 body 內容的目的 CustometRequestWrapper requestWrapper = new CustometRequestWrapper(httpServletRequest); chain.doFilter(requestWrapper, response); } @Override public void destroy() { } private class CustometRequestWrapper extends HttpServletRequestWrapper { public CustometRequestWrapper(HttpServletRequest request) { super(request); } @Override public ServletInputStream getInputStream() throws IOException { byte[] bytes = new byte[0]; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); return new ServletInputStream() { @Override public boolean isFinished() { return byteArrayInputStream.read() == -1 ? true : false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; } } }
完成如上配置後,再次測試,結果以下:
搞定! 因爲網絡問題,拉去最新數據時有點慢,須要多刷新幾回。。。