8.3 编写简单的消息生产者和消费者

现在我们已经了解完Spring Cloud Stream中的基本组件,接下来看一个简单的Spring Cloud Stream示例。对于第一个例子,我们将要从组织服务传递一条消息到许可证服务。在许可证服务中,
唯一要做的事情就是将日志消息打印到控制台。
    另外,在这个例子中,因为只有一个Spring Cloud Stream发射器(消息生成者)和接收器(消息消费者),所以我们将要采用Spring Cloud提供的一些便捷方式,让在组织服务中建立发射器以及在许可证服务中建立接收器变得更简单。
    8.3.1 在组织服务中编写消息生产者
    我们首先修改组织服务,以便每次添加、更新或删除组织数据时,组织服务将向Kafka主题(topic)发布一条消息,指示组织更改事件已经发生。图8-4突出显示了消息生产者,并构建在图8-3所示的通用Spring Cloud Stream架构之上。
 
 
 
图8-4 当组织服务数据发生变化时,它会向Kafka发布消息
    发布的消息将包括与更改事件相关联的组织ID,还将包括发生的操作(添加、更新或删除)。
    
需要做的第一件事就是在组织服务的Maven pom.xml文件中设置Maven依赖项。pom.xml文件可以在organization-service目录中找到。在pom.xml中,需要添加两个依赖项:一个用于核心Spring Cloud Stream库,另一个用于包含Spring Cloud Stream Kafka库。
    <dependency>   
        <groupId>org.springframework.cloud</groupId>   
        <artifactId>spring-cloud-stream</artifactId>    
        </dependency> 
      <dependency>   
     <groupId>org.springframework.cloud</groupId>   
    <artifactId>spring-cloud-starter-stream-kafka</artifactId> 
    </dependency>
    定义完Maven依赖项,就需要告诉应用程序它将绑定到Spring Cloud Stream消息代理。这可以通过使用@EnableBinding注解来标注组织服务的引导类Application(在organization- service/src/main/java/com/thoughtmechanix/organization/Application.java中)来完成。代码清单8-1展示了组织服务的Application类的源代码。
    代码清单8-1 带注解的Application类
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import javax.servlet.Filter;
 
⇽--- @EnableBinding注解告诉Spring Cloud Stream将应用程序绑定到消息代理
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
@EnableBinding(Source.class)
public class Application {
    @Bean
    public Filter userContextFilter() {
        UserContextFilter userContextFilter = new UserContextFilter();
        return userContextFilter;
    }
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
 
    在代码清单8-1中,@EnableBinding注解告诉Spring Cloud Stream希望将服务绑定到消息代理。@EnableBinding注解中的Source.class告诉Spring Cloud Stream,该服务将通过在Source类上定义的一组通道与消息代理进行通信。记住,通道位于消息队列之上。Spring Cloud Stream有一个默认的通道集,可以配置它们来与消息代理进行通信。
    到目前为止,我们还没有告诉Spring Cloud Stream希望将组织服务绑定到什么消息代理。本章很快就会讲到这一点。现在,我们可以继续实现将要发布消息的代码。
    消息发布的代码可以在organization-service/src/com/thoughtmechanix/organization/events/source/SimpleSourceBean.java中找到。代码清单8-2展示了这个SimpleSourceBean类的代码。
    代码清单8-2 向消息代理发布消息
       // 为了简洁,省略了import语句
    @Component
public class SimpleSourceBean {
    private Source source;
 
    private static final Logger logger = LoggerFactory.getLogger(SimpleSourceBean.class);
     ⇽--- Spring Cloud Stream将注入一个Source接口,以供服务使用
    @Autowired
    public SimpleSourceBean(Source source){
        this.source = source;
    }
 
    public void publishOrgChange(String action,String orgId){
       logger.debug("Sending Kafka message {} for Organization Id: {}", action, orgId);
        ⇽--- 要发布的消息是一个Java POJO     
        OrganizationChangeModel change =  new OrganizationChangeModel(
                OrganizationChangeModel.class.getTypeName(),
                action,
                orgId,
                UserContext.getCorrelationId());
        ⇽--- 当准备发送消息时,使用Source类中定义的通道的send()方法
        source.output().send(MessageBuilder.withPayload(change).build());
    }
}
   
在代码清单8-2中,我们将Spring Cloud Source类注入代码中。记住,所有与特定消息主题的通信都是通过称为通道的Spring Cloud Stream结构来实现的。通道由一个Java接口类表示。在代码清单8-2中,我们使用的是Source接口。Source是Spring Cloud定义的一个接口,它公开了一个名为output()的方法。当服务只需要发布到单个通道时,Source接口是一个很方便的接口。output()方法返回一个MessageChannel类型的类。MessageChannel代表了如何将消息发送给消息代理。本章稍后将介绍如何使用自定义接口来公开多个消息传递通道。
    消息的实际发布发生在publishOrgChange()方法中。此方法构建一个Java POJO,名为OrganizationChangeModel。本章不会展示OrganizationChangeModel的代码,因为这个类只是一个包含3个数据元素的POJO。
    动作(action)——这是触发事件的动作。我在消息中包含了这个动作,以便让消息消费者在处理事件的过程中有更多的上下文。
    组织ID(organization ID)——这是与事件关联的组织ID。
    关联ID(correlation ID)——这是触发事件的服务调用的关联ID。应该始终在事件中包含关联ID,因为它对跟踪和调试流经服务的消息流有极大的帮助。
    当准备好发布消息时,可使用从source.output()返回的MessageChannel的send()方法:
source.output().send(MessageBuilder.withPayload(change).build());
 
send()方法接收一个Spring Message类。我们使用一个名为MessageBuilder的Spring辅助类来接收OrganizationChangeModel类的内容,并将它转换为Spring Message类。
    这就是发送消息所需的所有代码。然而,到目前为止,这一切都感觉有点儿像魔术,因为我们还没有看到如何将组织服务绑定到一个特定的消息队列,更不用说实际的消息代理。上述的这一切都是通过配置来完成的。代码清单8-3展示了这一配置,它将服务的Spring Cloud Stream Source映射到Kafka消息代理以及Kafka中的消息主题。此配置信息可以位于服务的application.yml文件中,
也可以位于服务的Spring Cloud Config条目中。
    
代码清单8-3 用于发布消息的Spring Cloud Stream配置
spring:
  cloud:
    stream:  ⇽--- stream.bindings是所需配置的开始,用于服务将消息发布到Spring Cloud Stream消息代理   
      bindings:
        //output定义了图8-4中的通道
        output: ⇽--- output是通道的名称,映射到在代码清单8-2中看到的source.output()通道
            destination:  orgChangeTopic ⇽--- 这是要写入消息的消息队列(或主题)的名称    
            content-type: application/json ⇽--- content-type向Spring Cloud Stream提供了将要发送和接收什么类型的消息的提示(在本例中是JSON)  
      ⇽--- stream.bindings.kafka属性告诉Spring,将使用Kafka作为服务中的消息总线(可以使用RabbitMQ作为替代)
      kafka:
        //这里binder定义了图8-4中的绑定器
        binder:
          ⇽--- Zknodes和brokers属性告诉Spring Cloud Stream,Kafka和ZooKeeper的网络位置
          zkNodes: localhost 
          brokers: localhost
 
#Setting the logging levels for the service
 
代码清单8-3中的配置看起来很密集,但很简单。代码清单8-3中的配置属性spring.stream.bindings.output将代码清单8-2中的source.output()通道映射到要与之通信的消息代理上的主题orgChangeTopic。它还告诉Spring Cloud Stream,发送到此主题的消息应该被序列化为JSON。Spring Cloud Stream可以以多种格式序列化消息,包括JSON、XML以及Apache基金会的Avro格式。
    
代码清单8-3中的配置属性spring.stream.bindings.kafka告诉Spring Cloud Stream,将服务绑定到Kafka。子属性告诉Spring Cloud Stream,Kafka消息代理和运行着Kafka的Apache ZooKeeper服务器的网络地址。
    我们已经编写完通过Spring Cloud Stream发布消息的代码,并通过配置来告诉Spring Cloud Stream它将使用Kafka作为消息代理,那么接下来让我们来看看,组织服务中消息的发布实际发生在哪里。这项工作将在organization-service/src/main/java/com/thoughtmechanix/organization/ services/OrganizationService.java中的OranizationServer类完成。代码清单8-4展示了这个类的代码。
    
代码清单8-4 在组织服务中发布消息
// 为了简洁,省略了import语句
@Service
public class OrganizationService {
    @Autowired
    private OrganizationRepository orgRepository;
    ⇽--- Spring的自动装配用于将SimpleSourceBean注入组织服务中    
    @Autowired
    SimpleSourceBean simpleSourceBean;
 
    public void saveOrg(Organization org){
        org.setId( UUID.randomUUID().toString());
 
        orgRepository.save(org);
         ⇽--- 对服务中修改组织数据的每一个方法,调用simpleSourceBean. publishOrgChange()
        simpleSourceBean.publishOrgChange("SAVE", org.getId());
    }
}    
    
应该在消息中放置什么数据
    我从团队中听到的一个最常见的问题是,当他们第一次开始消息之旅时,应该在消息中放置多少数据。我的答案是,这取决于你的应用程序。正如读者可能注意到的,在我的所有示例中,我只返回已更改的组织记录的组织ID。我从来没有把数据更改的副本放在消息中。在我的例子中(以及我在电话通信领域中遇到的许多问题),执行的业务逻辑对数据的变化非常敏感。我使用基于系统事件的消息来告诉其他服务,数据状态已经发生了变化,但是我总是强制其他服务重新到主服务器(拥有数据的服务)上来检索数据的新副本。这种方法在执行时间方面是昂贵的,但它也保证我始终拥有最新的数据副本。在从源系统读取数据之后,所使用的数据依然可能会发生变化,但这比在队列中盲目地消费信息的可能性要小得多。
    要仔细考虑要传递多少数据。开发人员迟早会遇到这样一种情况:传递的数据已经过时了。这些数据可能是陈旧的,因为出现某种问题导致它在消息队列待了太长时间,或者之前包含数据的消息失败了,并且消息中传入的数据现在处于不一致的状态(因为应用程序依赖于消息的状态,而不是底层数据存储中的实际状态)。如果要在消息中传递状态,还要确保在消息中包含日期时间戳或版本号,以便使用数据的服务可以检查传递的数据,并确保它不会比服务已拥有的数据副本更旧(记住,数据可以不按顺序进行检索)。
 
 
 
posted @ 2019-12-03 10:55  mongotea  阅读(377)  评论(0编辑  收藏  举报