聲明:
1.本節將會通過Spring Cloud Bus來將配置更新的事件進行發布,從而達到在更新配置后,使得所有服務都去更新配置的效果,由于配置中心集成在Eureka中,且會以Kafka作為Spring Cloud Bus的基礎,所以本節將會使用Spring Cloud Netflix Eureka + Spring Cloud Config + Spring Cloud Bus + Spring Kafka來完成本節內容,Kafka也需要Zookeeper的環境基礎,所以你還得整個Zookeeper。
2.入門級文檔,更多內容會持續更新,不足之處,望不吝指點
一、Spring Cloud Bus介紹
Spring Cloud Bus
就是一個消息總線,也就是一個廣播,任何對象都可以接收這條總線上的任何廣播消息,同樣也可以發布消息出去。內部是使用Spring Cloud Stream
來實現,也就是說Spring Cloud Bus
不過是Spring Cloud Stream
的一個廣播性用法,主要用于在服務間共享事件,使得一個事件不單單只在一個服務上被處理,而是可以擴大到整個分布式應用上去。
目前Spring Cloud Bus
支持RabbitMQ
和Kafka
兩種消息中間件。
二、Spring Cloud Bus自帶事件
-
Spring Cloud Bus
內部自帶了幾個比較重要的事件:-
RemoteApplicationEvent
這是Spring Cloud Bus
支持的遠程事件的超類,只有繼承該類的事件類才能夠被發布到消息隊列中去,本身是一個抽象類,無法實例化。 -
RefreshRemoteApplicationEvent
這是配置刷新事件,父類是RemoteApplicationEvent
,其他服務如果接收到這個事件,并且確定是自己應收的,就會自動進行配置的刷新 -
EnvironmentChangeRemoteApplicationEvent
環境變化事件,父類是RemoteApplicationEvent
-
- 其他事件:
-
AckRemoteApplicationEvent
確認接收事件,當服務確實接收到一個事件后(指自己應當接收的),就會回返一條消息告訴發送者我接收了這個事件。 -
UnknownRemoteApplicationEvent
,未知遠端事件,當服務接收到一條消息,并嘗試反序列化該事件時發現這個事件它不認識,就會產生該事件。 -
SentApplicationEvent
當在發送一個事件的時候產生該事件。
-
三、事件接收者
事件接收者指這個事件應當被哪個服務接收,這與廣播機制并不沖突,就比如,我在廣播中找“李四”,那么只有“李四”聽到了消息應當回應,其他人其實也聽得到,但是因為不是“李四”,所有沒有必要回應罷了。
事件接收者和事件的發送者在Spring Cloud Bus
中都由一串特殊的字符串構成,其格式為app:index:id
,其中:
app
指的是vcap.application.name
或者是spring.application.name
(寫在前的優先級高)
index
是vcap.application.instance_index
或spring.application.index
或local.server.port
或server.port
或0
id
是vcap.application.instance_id
或者是一個不重復的隨機值
注:**
是通配符
例如:service:**
表示事件的接收者是叫service
服務的所有實例
四、端點
Spring Cloud Bus
一共開了4個端點,分別是/bus/refresh
,/bus/env
,/actuator/bus-refresh
和/actuator/bus-env
,它們都只接受Post請求,后兩者需要使用management.endpoints.web.exposure.include
來開啟。它們會分別觸發RefreshRemoteApplicationEvent
和EnvironmentChangeRemoteApplicationEvent
事件。
附:
/actuator/bus-env
可以接受一個Json格式的數據來進行環境的變更,其格式如下:
{
"name": "key1",
"value": "value1"
}
五、發布你的自定義事件
你肯定不滿足只發布自帶的那幾個事件,你可能想發布自己的事件
- 創建你自己的事件并使其繼承
RemoteApplicationEvent
,并且保證公有的無參構造方法存在,例如:
public class TestRemoteEvent extends RemoteApplicationEvent {
public TestRemoteEvent(){}
public TestRemoteEvent(Object source, String originService, String destinationService){
super(source , originService , destinationService);
}
}
- 在需要接受該方法的服務中將該事件注冊給
Spring Cloud Bus
這時候你需要使用到@RemoteApplicationEventScan
注解,該注解使用方法同@ComponentScan
,把該事件所在的包名配置上即可
注意:事件的發送者和接受者都要有這個事件,唯一不同的是,發送者(如果不需要的話)可以不用注冊該事件給Spring Cloud Bus
六、配置
#開啟Spring Cloud Bus
spring.cloud.bus.enabled=true
#消息發送與接收的頻道
spring.cloud.bus.destination=SpringCloudBus
#更多配置可以嘗試spring.cloud.stream
#kafka使用者可以使用下列配置
spring.kafka.bootstrap-servers=localhost:9092
七、使用Spring Cloud Bus實現配置自動刷新功能
- 實現原理:
由于訪問/actuator/bus-refresh
可以發布配置更新事件,所以我們就需要實現在Git倉庫更新時,讓其訪問/actuator/bus-refresh
就行了。 - 依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
- 配置
- 服務中心(其他配置不列出)
spring: kafka: # kafka的地址,我的啟動在本地9093端口 bootstrap-servers: localhost:9093 cloud: bus: refresh: #服務中心接收事件,但不響應刷新 enabled: false env: #服務中心接收事件,但不響應環境變化 enabled: false #開啟spring cloud bus enabled: true
- 遠端配置(application.yaml)
management: endpoints: web: exposure: include: health , info spring: kafka: bootstrap-servers: localhost:9093
- Git遠端倉庫WebHook配置
WebHook配置是各大遠端倉庫(Github、Gitee等)的基本功能,其作用是在倉庫更新時自動調用一個接口,此處我將以Gitee作為示例:
點擊主界面上的管理
點擊WebHooks后點擊添加
配置WebHook
注意:回調地址應當配置為觸發RefreshRemoteApplicationEvent
刷新事件的地址,所以應當為http://host:port/xxx/actuator/bus-refresh,xxx代表server.servlet.context-path
。但是!!!由于Gitee的回調會附帶一大串Json格式的信息,所以直接使用actuator/bus-refresh
接口會報無法正常解析Json的問題,但是由于Gitee回調附帶的信息中包含了commit的信息,所以我們可以自己開一個接口,對回調的數據進行解析,根據解析出來的文件修改信息,我們可以實現對指定服務進行事件的發布,而不是一股腦的全部發布。比如本次提交中修改了service1.properties
那么我便將事件的目標設為service1:**
,如果我修改了application.yaml
那么我便將事件的目標設為**
- 針對Gitee的特殊回調接口
/**
* @author mtk
* 針對WebHook的回調接口
*/
@RestController
@RequestMapping("/web-hook")
public class WebHookController {
//自定義的Bus遠端事件發布工具類
private BusRemoteEventPublisher busRemoteEventPublisher;
@Autowired
public WebHookController(BusRemoteEventPublisher busRemoteEventPublisher){
this.busRemoteEventPublisher = busRemoteEventPublisher;
}
/**
* 針對Gitee的WebHook的回調接口
* @param jsonInfo 回調數據
* @return 簡易的執行結果
*/
@PostMapping("/refresh-config")
public String refreshBus(@RequestBody Map<String,Object> jsonInfo){
//解析json
List<Object> commits;
if((commits = (List<Object>) jsonInfo.get("commits")) != null){
//獲取修改的文件的文件名
Set<String> modifiedFiles = new HashSet<>();
commits.forEach(item -> {
Map<String,Object> commit;
if(item instanceof Map){
commit = (Map<String, Object>) item;
List<String> modified;
if((modified = (List<String>) commit.get("modified")) != null){
modifiedFiles.addAll(modified);
}
}
});
//文件過濾
//去除非配置文件
List<String> modifiedSettingFileBaseNames = modifiedFiles.stream().filter(item -> {
String e = FilenameUtils.getExtension(item);
return "yaml".equals(e) || "yml".equals(e) || "properties".equals(e);
}).map(FilenameUtils::getBaseName).collect(Collectors.toList());
//是否變更了全局配置文件
boolean isMatchGlobalEvent = modifiedSettingFileBaseNames.stream().anyMatch(item -> {
if("application".equals(item)) {
busRemoteEventPublisher.publish(RefreshRemoteApplicationEvent.class, null);
return true;
}
return false;
});
if(isMatchGlobalEvent) return "refreshed all services";
//對每個服務的刷新事件進行獨立發布
modifiedSettingFileBaseNames.forEach(item -> {
busRemoteEventPublisher.publish(RefreshRemoteApplicationEvent.class , item+":**");
refreshedService.add(item);
});
return "refreshed service: "+modifiedSettingFileBaseNames.toString();
}
//json格式錯誤
return "error";
}
}
- 到此為止,基本上就已經完成了,如果你想看到效果,你可以給需要刷新配置的地方加上@RefreshScope注解,比如:
@RestController
@RequestMapping("/hello")
@RefreshScope
public class HelloController {
@Value("${cn.mtk.hello}")
private String hello;
@GetMapping("/ph")
public String printHello(){
return hello;
}
}
如果你變更過遠端倉庫上的配置文件,并修改了
cn.mtk.hello
這一項配置,那么你將會在/hello/ph
上看到更新后的結果
注意:如果你發現配置并沒有刷新,但所有步驟都沒有問題,那么你得考慮下是不是消費者沒有正常連接到Kafka,你可以通過調整日志等級為來查看是否有隱藏掉的錯誤日志logging.level.root=DEBUG
,或者開啟一個Kafka消費者控制臺來查看消息的發送情況(如果一切都是默認配置的話)kafka-console-consumer --bootstrap-server localhost:9092 --from-beginning --topic SpringCloudBus --partition 0
,如果發現確實是Kafka問題,并且各種重啟無效后,你可以嘗試刪除SpringCloudBus
這個話題。
$ zkcli
$ rmr /brokers/topics/SpringCloudBus
$ quit
附:
- BusRemoteEventPublisher
/**
* @author mtk
* 便捷的bus遠端事件發布工具
*/
public class BusRemoteEventPublisher {
private ApplicationEventPublisher applicationEventPublisher;
private BusProperties busProperties;
public BusRemoteEventPublisher(ApplicationEventPublisher applicationEventPublisher , BusProperties busProperties){
this.applicationEventPublisher = applicationEventPublisher;
this.busProperties = busProperties;
}
public void publish(Class<? extends RemoteApplicationEvent> eventClass, String destinationService){
try{
RemoteApplicationEvent event = eventClass.getDeclaredConstructor(Object.class , String.class , String.class).newInstance(this , busProperties.getId() , destinationService);
applicationEventPublisher.publishEvent(event);
}catch(Exception e){
e.printStackTrace();
}
}
}
參考文檔:
[1] Spring Cloud Bus