消息總線: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。但是?。。?/strong>由于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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。