【译】Spring Cloud Stream与事件驱动的微服务

397 阅读5分钟

使用Spring Cloud Stream和Apache Kafka消息代理来发布(publish)与消费(consume)讯息

导言

由于事件驱动架构需要预先规划,想要创建一个能整合流行的事件流平台的脚手架是相当复杂的。Spring Cloud Stream是一个消息驱动的微服务应用框架,它为RabbitMQ、Apache Kafka、Kafka Streams、Amazon Kinesis等各种消息代理(broker)提供了绑定器的引用。此框架能进行简化,并使我们轻松构建不同平台的消息发布与消费;我们不再需要被所选平台的各种引用细节困扰,也能使用已经熟悉的Spring习语与接口。

在这篇文章中我们将使用Apache Kafka消息代理。

Jay Kreps选择使用作家 Franz Kafka (弗兰茨.卡夫卡)的姓氏来为此软体命名,因为它是”一个能最佳化写作的系统”,而他喜欢 Kafka 的作品。

来源: Wikipedia

在我之前的文章中,我们在每次客户下订时使用Spring应用事件(Spring Application Events)触发电子邮件的发送。

使用Spring Application Events 与 Apache FreeMarker 自动发送邮件

现在,我们将基于上一篇文章继续建构以探索Spring Cloud Stream架构。接下来我们会将该程序拆分为两个使用消息进行通信的微服务;我们将创建User(用户)服务来处理用户管理,并创建Notification(通知)服务来发送不同的通知,在本文中通知即为电子邮件。 用户服务是消息的生产者(发布者),它会在每次新用户帐户建立时向Kafka的topic发布一条消息。通知服务是消息的消费者,它将消费来自消息队列的消息并对这些消息做出反应,且不会直接被用户服务调用。 我们还能通过使用消息通信添加新功能:通过让用户服务监听消息队列上的消息,我们能对用户服务的变化做出反应并调整。

依赖项

Spring Cloud Stream应用程序具有中间件中立的核心,并通过由外部代理公开的目的地之间的绑定进行通信;而建立绑定所需的具体代理细节是透过引用(implementation) binder (绑定器) 来处理的。因此我们需要将Kafka绑定器的引用和其他所需的依赖项一起添加到用户服务和通知服务中的 gradle.buildpom.xml 文件中。

dependencies {
  ...
   implementation 'org.springframework.cloud:spring-cloud-stream'
   implementation 'org.springframework.cloud:spring-cloud-stream-binder-kafka'
  ...
}

通知服务

由于通知服务需要消费用户服务所发布的消息,我们来着手创建一个 消费者(Consumer) 对象(bean)吧。消费者代表一个" 接受单个输入参数" 的操作。在这个范例中,我们使用UserDto保存注册用户的相关信息,例如用户的姓名和电子邮件地址。

@RequiredArgsConstructor
@Slf4j
@Component
public class NotificationEventConsumer {

    private final EmailService emailService;

    @Bean
    public Consumer<UserDto> eventConsumer() {
        return user -> {
            log.info("Received new message from Kafka topic");
            Map<String, Object> model = new HashMap<>();
            model.put("user", user);
            var data = new NotificationData.NotificationDataBuilder()
                    .subject("Account confirmation")
                    .toEmail(user.getEmail())
                    .model(model)
                    .template("account-confirmation")
                    .build();

            try {
                emailService.sendEmail(data);
            } catch (IOException | TemplateException e) {
                e.printStackTrace();
            }
        };
    }
}

绑定是来源与目标的桥樑,在函数式编程模型(functional programming model)中,绑定名称默认为:

  • input — <functionName> + -in- + <index>
  • output — <functionName> + -out- + <index>

** in out 对应绑定的类型,输入或输出。

为了提升可读性,我们可以使用以下属性将 隐式绑定名称 映射到 显式绑定名称 来使绑定的名称更具描述性:

spring.cloud.stream.bindings.eventConsumer-in-0.destination= notification-events

用户服务

由于这是一个按需随选事件,我们需要导入一个 StreamBridge 对象(bean)。它使我们可以发送数据到输出绑定(output binding),从而在非stream 的应用程序与 Spring Cloud Stream 间建立一座桥樑。 为了触发能够创建来源绑定(sourse binding)的事件,我们使用 spring.cloud.stream.source=supplier 属性声明来源的名称,并使用spring.cloud.stream.bindings.supplier-out-0.destination=notification-events 属性给来源一个显式名称(名称与在通知服务中相同)。最后我们使用 streamBridge.send()方法传递一个 POJO,它将如同来自函式或供应者(Supplier)一样,通过相同的路径。

@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final StreamBridge streamBridge;

    @Transactional
    public void createUser(UserDto userDto) {
        var user = User.builder()
                .firstName(userDto.getFirstName())
                .lastName(userDto.getLastName())
                .email(userDto.getEmail())
                .build();
        userRepository.save(user);
        streamBridge.send("supplier-out-0", userDto);
    }
}

Docker

为了运行它,您需要安装Kafka和Zookeeper。我使用Docker来运行Kafka,并使用Zookeeper来运行其它微服务。这是一个 docker-compose.yml 的片段:

# ...
zookeeper:
    image: wurstmeister/zookeeper:latest
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka:latest
    ports:
      - "9092:9092"
    depends_on:
      zookeeper:
        condition: service_started
    environment:
      DOCKER_API_VERSION: 1.41
      KAFKA_ADVERTISED_HOST_NAME: kafka
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181

在您的微服务application.proeprties中使用spring.cloud.stream.kafka.binder.brokers=kafka属性来提供Kafka绑定器所连结的broker列表,默认为localhost。此外若您没有在默认端口9092上运行 Kafka,您可以使用spring.cloud.stream.kafka.binder.defaultBrokerPort属性。

现在我们已经设置好所有内容,可以向用户服务发送请求了。

img

我们可以在通知服务的日志中看到消息接收成功。

img

电子邮件也成功抵达:

img

请注意:由于Spring Cloud Stream使用了Spring Retry库,当消息处理的过程中出现例外时,此框架将会多次尝试重传相同的消息(默认为3次)。

写在最后

Spring Cloud Stream使处理异步消息变得更为容易。它使用了函数式编程模型;Kafka的发布者、消费者与处理者(Producers、Consumer 和 Processors)被定义为Java的函数接口供应者、消费者与函式(Suppliers、Consumer 和 Functions)。您只需要定义一个供应者、消费者或Spring Beans函数,而绑定器将负责整合您所选择的代理,让您的代码能够专注于商业逻辑并保持中间件中立。

完整demo项目的源代码可在GitHub上取得。