消息總線:Spring Cloud Bus

聲明:
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支持RabbitMQKafka兩種消息中間件。


二、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(寫在前的優先級高)
indexvcap.application.instance_indexspring.application.indexlocal.server.portserver.port0
idvcap.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來開啟。它們會分別觸發RefreshRemoteApplicationEventEnvironmentChangeRemoteApplicationEvent事件。
附:
/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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373