SpringCloudBus之消息总线




写在前面

该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud bus 一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!文章也参考了 Spring-cloud-config 的官方文档。


Spring cloud bus 使用轻量级消息代理连接分布式系统的节点。这可以用来广播状态更改(例如配置更改)或其他管理指令。一个关键的想法是,总线就像一个分布式执行器,用于扩展的SpringBoot应用程序。不过,它也可以用作应用程序之间的通信通道。该项目为启动者提供了AMQP代理或Kafka作为传输。


1. 入门

Spring Cloud Bus 在类路径上检测到自己,它会通过 Spring Boot autoconfiguration 来工作。要启用消息总线,只需要将 spring-cloud-starter-bus-amqp 或者 spring-cloud-starter-bus-kafka 加入到依赖项中。Spring cloud 将负责其余的事情。确保代理(RabbitMQKafka)可用并已配置。在本地主机上运行时,您不需要做任何事情。如果您远程运行,请使用Spring Cloud连接器或Spring Boot约定来定义消息代理凭据,如下面的Rabbit示例所示:

spring:
  rabbitmq:
    host: mybroker.com
    port: 5672
    username: user
    password: secret

导入依赖:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

在导入以上依赖之后,我们需要写下我们的测试。

  1. 构建 rabbit 消息监听器:

    @Component
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue("testSpringCloudRabbit"), exchange = @Exchange("exchange"), key = "routingKey")
    })
    public class Receiver {
    
        @RabbitHandler
        public void process(String msg){
            System.out.println("receiving :  " + msg);
        }
    }
    
  2. 发送消息:

    @RestController
    public class TestController {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        @GetMapping("/send")
        public void send(@RequestParam String msg){
            amqpTemplate.convertAndSend("exchange", "routingKey", msg);
        }
    
    }
    

有关 Rabbit 的一些概念,需要参考其它文章了。

启动程序,访问 /send?msg=helloworld ,控制台能成功打印出消息。似乎这只是一个使用 RabbitMq 的用例,那么在 Spring Cloud 的环境下,它能够给我们什么便利呢?

  • 总线能够支持将消息发送到监听某个特定服务的所有节点

  • /bus/* actuator 命名空间有一些 HTTP 端点,目前有两个实现:

    • /bus/env 通过发送键值对来更新每个节点的 Spring 环境

    • /bus/refresh 重新加载每个应用程序的配置,就好像我们之前在 Spring cloud config中提到过的通过访问服务的 /refresh端点来实现动态刷新一样

      想象一下,在之前的配置中心,当有文件改变,我们需要实现动态更新,则需要手动去访问需要更新的每个服务的 /refresh 端点(Git 仓库的 Web hook可以避免手动更新)。但使用消息总线,则连接到消息总线上的多个应用(单服务多实例)都会接收到更新请求。


2. Bus 端点

Spring Cloud Bus提供了两个端点,/actuator/bus-refresh/actuator/bus-env,它们分别对应于Spring Cloud Commons中的单个actuator端点,/actuator/refresh/actuator/env。为了暴露这两个端点,需要额外的配置:

management.endpoints.web.exposure.include=bus-refresh,bus-env

2.1 Bus Refresh 端点

/actuator/bus-refresh 端点清除 RefreshScope 缓存,重新绑定 @ConfigurationProperties

2.2 Bus Env 端点

actuator/bus-env端点使用指定的跨多个实例的键/值对更新每个实例环境。


实践


创建一个可配置化属性类:

@ConfigurationProperties(prefix = "duofei.test")
public class ConfigPro {

    private String pro;

    public String getPro() {
        return pro;
    }

    public void setPro(String pro) {
        this.pro = pro;
    }
}

使用 @ConfigurationProperties 注解生成自己的配置元数据文件,需要引入下面的依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

yaml 文件中添加如下数据:

duofei:
  test:
    pro: hello,world

Controller 中新增如下测试接口:

    @Autowired
    private ConfigPro configPro;

	@GetMapping("/pro")
    public String pro(){
        return configPro.getPro();
    }

对了,不能忘了将它声明成 bean,在启动类中加入:

    @Bean
    public ConfigPro configPro(){
        return new ConfigPro();
    }

访问接口,能够得到 hello,world 值返回。现在,通过 /actuatorbus-env 端点更新该值,使用 curl 工具请求该接口:

curl -X POST http://192.168.3.18:60022/actuator/bus-env -H "Content-Type:application/json" -d {\"name\":\"duofei.test.pro\",\"value\":\"1111\"}

这里的 post 传参是固定格式,{“name":"…", “value”:"…"},至于反斜杠是因为在我这里,单单使用双引号是无法传递到服务器的,我并不确定这是否与我使用的控制台工具有关。

此时,重新访问该接口,返回 ‘1111’;


3. 定位一个实例

应用程序的每个实例都有一个服务ID,可以使用spring.cloud.bus.id设置其值。值应该是一个冒号分隔的标识符列表,按从最不特定到最特定的顺序排列。默认值是由环境作为spring.application.nameserver.port(或spring.application.index,如果设置)的组合构造的。ID的默认值以 app:index:index的形式构造,其中:

  • appvcap.application.name, 如果存在的话,或spring.application.name

  • indexvcap.application.instance_index(如果存在),或者spring.application.index,local.server.port, server.port,或0(按此顺序)。

  • idvcap.application.instance_id(如果存在)或一个随机值。

当我们在使用上述的 HTTP 端点时,我们可以只针对某个实例执行操作,端点接收 destination 路径参数。destination 的值是服务id,只有 destination 参数指定的服务才会处理消息,其它的服务实例会忽略这条消息。


4. 定位一个服务的所有实例

Spring PathMatcher中使用 “destination” 参数(路径分隔符为冒号 : )来确定实例是否处理消息。使用前面的示例,/bus-env/customers:**针对 “customers” 服务的所有实例,而不考虑服务ID的其余部分。


5. 服务ID 必须唯一

总线尝试两次以消除对事件的处理—一次来自原始 ApplicationEvent,一次来自队列。为此,它根据当前服务ID检查发送服务ID。如果一个服务的多个实例具有相同的ID,则不处理事件。在本地机器上运行时,每个服务都位于不同的端口上,该端口是ID的一部分。


6. 自定义事件

自定义的事件类需要继承 RemoteApplicationEvent

创建一个通知事件:

public class NotifyRemoteApplicationEvent extends RemoteApplicationEvent {
    public NotifyRemoteApplicationEvent(Object source, String originService) {
        super(source, originService);
    }
}

事件需要被扫描到,通过在启动类上使用 @RemoteApplicationEventScan({"com.duofei.event"}) 注解。尽管是需要远程处理该事件,但我们依然可以在该程序中配置事件监听器,然后通过启动多个实例来实现远程的监听。

/**
 * @author duofei
 * @date 2019/11/13
 */
@Component
public class LocalEventListener {

    @EventListener
    public void handleNotify(NotifyRemoteApplicationEvent event) {
        System.out.println("你:" + event.getSource());
        System.out.println("我:知道啦!" );
    }
}

controller 中新增接口,用于发布事件:

    @Autowired
    private ApplicationContext applicationContext;

	@GetMapping("/notify")
    public void notifyOthers(@RequestParam String  msg){
        // config-client 为 spring.application.name 的值
        applicationContext.publishEvent(new NotifyRemoteApplicationEvent(msg, "config-client"));
    }

启动两个实例(暂且名为 A, B),调用 A 实例的该接口,观察输出发现,A 能正常输出,但B 实例似乎并没有监听到该事件;

什么原因呢?我们查看 RemoteApplicationEvent 的子类,能够找到其他的子类,我们只需要研究其他子类是怎样去发布远程事件的,那是不是能解决我们的问题呢?

找到 RefreshRemoteApplicationEvent 事件,该事件是在调用 HTTP 端点的时候发布的,查看 /bus-refresh 对应的端点类 RefreshBusEndpoint 类,该类的具体代码如下:

@Endpoint(id = "bus-refresh") // TODO: document new id
public class RefreshBusEndpoint extends AbstractBusEndpoint {

	public RefreshBusEndpoint(ApplicationEventPublisher context, String id) {
		super(context, id);
	}

	@WriteOperation
	public void busRefreshWithDestination(@Selector String destination) { // TODO:
																			// document
																			// destination
		publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), destination));
	}

	@WriteOperation
	public void busRefresh() {
		publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
	}

}

很容易找到该类的发布事件是通过 ApplicationEventPublisher 类发布的。该类只有一个构造函数,我们只需要找到它在哪里构造的,看一下 ApplicationEventPublisher 是如何传入进来的。定位到BusRefreshAutoConfiguration 类,其中的以下代码定义了RefreshBusEndpoint Bean:

	@Configuration
	@ConditionalOnBean(ContextRefresher.class)
	@ConditionalOnClass(name = {
			"org.springframework.boot.actuate.endpoint.annotation.Endpoint",
			"org.springframework.cloud.context.scope.refresh.RefreshScope" })
	protected static class BusRefreshEndpointConfiguration {

		@Bean
		@ConditionalOnEnabledEndpoint
		public RefreshBusEndpoint refreshBusEndpoint(ApplicationContext context,
				BusProperties bus) {
			return new RefreshBusEndpoint(context, bus.getId());
		}

	}

呀!它也是使用ApplicationContext呀,嗯… 注意到了嘛?它的远程事件接收到了 BusProperties 的 id 值,作为构造函数的入参 (originService) 的值。原来,我理解错了,看来,我也需要一个这样的值,修改 controller 中的测试接口为如下:

    @Autowired
    private BusProperties busProperties;

	@GetMapping("/notify")
    public void notifyOthers(@RequestParam String  msg){
        applicationContext.publishEvent(new NotifyRemoteApplicationEvent(msg, busProperties.getId()));
    }

重新启动实例,运行。A,B是收到了事件,不过监听器处理时出现了异常:

org.springframework.messaging.converter.MessageConversionException: Could not read JSON: Cannot construct instance ofcom.duofei.event.NotifyRemoteApplicationEvent, problem: null source

远程事件构造函数需要一个 source ,找了好久,才发现 RemoteApplicationEvent 类上有如下注解:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonIgnoreProperties("source")

也就是 无论如何,sorce 我是传不过去的,那么 我需要为 NotifyRemoteApplicationEvent 新增一个构造函数,将该类改为如下:

public class NotifyRemoteApplicationEvent extends RemoteApplicationEvent {

    private String msg;

    private NotifyRemoteApplicationEvent(){}

    public NotifyRemoteApplicationEvent(String msg, Object source, String originService) {
        super(source, originService);
        this.msg = msg;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

问题解决了,反观RemoteApplicationEvent 子类都会有一个无参构造,哪怕是 private 的。

测试接口中则需要这样发布事件:

applicationContext.publishEvent(new NotifyRemoteApplicationEvent(msg, new Object() , busProperties.getId()));

我想这是合理的,因为需要的数据可以通过定义事件的其它属性来获得。当然,也不乏其它的看法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Bus是一个用于在分布式系统中传播状态变化的消息总线。它基于Spring Cloud Stream和Spring Cloud Config构建,可以将消息广播到整个系统中的所有服务实例。通过使用Spring Cloud Bus,可以实现配置的动态刷新、事件的传播和集群中的状态同步。 下面是使用Spring Cloud Bus自定义消息总线的步骤: 1. 添加依赖:在项目的pom.xml文件中添加Spring Cloud Bus的依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> ``` 2. 配置消息代理:在应用的配置文件中配置消息代理,例如使用RabbitMQ作为消息代理: ```yaml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest ``` 3. 发送自定义消息:在需要发送自定义消息的地方,使用Spring Cloud Bus提供的API发送消息。例如,可以使用`/actuator/bus-refresh`端点发送刷新配置的消息: ```shell curl -X POST http://localhost:8080/actuator/bus-refresh ``` 4. 接收自定义消息:在需要接收自定义消息的地方,使用Spring Cloud Bus提供的注解和监听器来接收消息。例如,可以使用`@RefreshScope`注解来刷新配置: ```java @RefreshScope @RestController public class ConfigController { // ... } ``` 通过以上步骤,您可以使用Spring Cloud Bus自定义消息总线来实现配置的动态刷新、事件的传播和集群中的状态同步。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值