对原生 Kafka 的扩展 Kafka-Plus("KP")

396 阅读4分钟

对原生 Kafka 的扩展 Kafka-Plus("KP")

框架的整体设计哲学: Less is More。旨在做到 简洁不简单

项目 kafka-plus

示例工程

kafkaplus-spring-boot-starter-examples

kafkaplus-spring-boot3-starter-examples

0.动机

前提: 本文研究和探讨的是原生 Kafka 并非实际开发使用更多的 spring-kafka

0.1.原生 kafka 的痛点

  • 不管是 Producer 还是 Consumer 甚至是 Admin 在使用的过程中,都不可避免的需要配置一个 PropertyMap 对象;
    • 这个即使对有开发经验的老手来说,也是一个不小的负担,每次需要明确要使用哪些参数以及其对应的 参数名(config.key)参数值(config.value)
  • 配置以及写起来也很繁琐;

0.2.扩展点

基于以上种种痛点 Kafka-Plus 简称 KP 应运而生。

  • 所有的 API 采用流式(链式)的方式构建
    • AdminBuilder
    • ConsumerBuilder
    • ProducerBuilder
      • ProducerRecordBuilder
  • Kafka 常用的 config.key 都转义为枚举,方便大家使用
    • 减少使用的难度
    • Enum
      • x.enums.Kafka.Mode
      • x.enums.Kafka.Bootstrap
        • x.enums.Kafka.Bootstrap.Server
      • x.enums.Kafka.Consumer
      • x.enums.Kafka.Producer
  • 所有的 API 都交由 KafkaEngine 来发起,方便管理

接下来我们就动手实操吧, Let's Go


1.使用

1.1.版本号

https://central.sonatype.com/artifact/io.github.photowey/kafka-plus/versions
https://central.sonatype.com/artifact/io.github.photowey/kafkaplus-spring-boot-starter/versions

1.2.依赖

1.2.1.Kafka-Clients

<!-- ${kafka-plus.version} == ${latest.version} -->
<dependency>
    <groupId>io.github.photowey</groupId>
    <artifactId>kafka-plus-engine</artifactId>
    <version>${kafka-plus.version}</version>
</dependency>

1.2.2.Spring Boot

  • version < 3.x
<!-- ${kafka-plus.version} == ${latest.version} -->
<dependency>
    <groupId>io.github.photowey</groupId>
    <artifactId>kafkaplus-spring-boot-starter</artifactId>
    <version>${kafka-plus.version}</version>
</dependency>

1.2.3.Spring Boot

  • version >= 3.x
<!-- ${kafka-plus.version} == ${latest.version} -->
<dependency>
    <groupId>io.github.photowey</groupId>
    <artifactId>kafkaplus-spring-boot3-starter</artifactId>
    <version>${kafka-plus.version}</version>
</dependency>

2.APIs

2.1.KafkaEngine

如何获取一个 KafkaEngine 实例?

2.1.1.原生

KafkaEngine kafkaEngine = KafkaEngineHolder.INSTANCE.kafkaEngine()

2.1.2.Spring Boot

  • 依赖查找
  • 依赖注入
  • 实现 KafkaEngineAware 接口
    • 通常还可以配合 KafkaEngineGetter 一起使用
// 1.BeanFactory 依赖查找
KafkaEngine kafkaEngine = this.beanfactory.getBean(KafkaEngine.class);

// 2.Autowired 依赖注入
@Autowired 
private KafkaEngine kafkaEngine;

// 3....

2.2.Admin

我们如何创建一个 Admin 实例呢?

@Test
void testAdmin() {
    KafkaEngine kafkaEngine = super.kafkaEngine();

    // 1.通过 KafkaEngine 实例获取 AdminService 实例
    // 2.通过 AdminService 发起一个 创建 Admin 的动作
    // 3.流式(链式) 配置指定的参数
    try (Admin admin = kafkaEngine.adminService().createAdmin()
         .boostrapServers(this.defaultBoostrapServers())
         .checkConfigs(super::testBoostrapServers)
         .build()) {

        Assertions.assertNotNull(admin);
    }
}

值得注意的是: checkXxx 方法采用 钩子函数 的方式给开发者一个机会,可以校验我们的配置是否合理或正确,完全由开发者自己掌控,框架只负责在合适的时机触发。

2.3.Topic

2.3.1.Create

创建 Topic

  1. 创建 Admin 实例
  2. 创建 NewTopic 实例
  3. 通过 Admin 实例创建 Topic
@Test
void testCreateTopic() throws Exception {
    KafkaEngine kafkaEngine = super.kafkaEngine();

    // 1.创建 Admin 实例
    try (Admin admin = kafkaEngine.adminService().createAdmin()
         .boostrapServers(this.defaultBoostrapServers())
         .checkConfigs(super::testBoostrapServers)
         .build()) {

        // 2.创建 NewTopic 实例
        NewTopic topic = kafkaEngine.adminService().createTopic()
            .topic(this.defaultTopic())
            .numPartitions(1)
            .replicationFactor(1)
            .build();

        // 3.通过 Admin 实例创建 Topic
        CreateTopicsResult topicsResult = admin.createTopics(Collections.singleton(topic));

        // topicsResult...
    }
}

2.3.2.Delete

删除 Topic

  1. 创建 Admin 实例
  2. 通过 Admin 实例删除 Topic
@Test
void testDeleteTopic() throws Exception {
    KafkaEngine kafkaEngine = super.kafkaEngine();

    // 1.创建 Admin 实例
    try (Admin admin = kafkaEngine.adminService().createAdmin()
         .boostrapServers(this.defaultBoostrapServers())
         .checkConfigs(super::testBoostrapServers)
         .build()) {

        // 2.通过 Admin 实例删除 Topic
        DeleteTopicsResult topicsResult = admin.deleteTopics(Collections.singleton(this.defaultTopic()));

        // topicsResult...
    }
}

2.4.Consumer

2.4.1.不带订阅 Topic

  1. 创建 KafkaConsumer 实例
  2. 手动订阅 Topic
@Test
void testConsumer() {
    KafkaEngine kafkaEngine = this.kafkaEngine();

    // 1.创建 KafkaConsumer 实例
    try (KafkaConsumer<String, String> consumer = kafkaEngine.consumerService().createConsumer()
         .boostrapServers(this.defaultBoostrapServers())
         .keyDeserializer(StringDeserializer.class)
         .valueDeserializer(StringDeserializer.class)
         .autoOffsetReset(Kafka.Consumer.AutoOffsetReset.EARLIEST)
         .groupId(this.defaultGroup())
         .autoCommit(true)
         .checkConfigs(super::testBoostrapServers)
         .build()) {

        // 2.订阅 Topic
        consumer.subscribe(Collections.singletonList(this.defaultTopic()));

        Assertions.assertNotNull(consumer);
    }
}

2.4.2带订阅 Topic

  1. 获取 KafkaConsumer 实例
    1. 在创建 KafkaConsumer 实例时,我们也可以一步到位
      1. ConsumerBuilder props(Properties props)
      2. ConsumerBuilder configs(Map<String, Object> configs)
    2. 如果现有的 Builder 满足不了您的开发配置,设计了扩展接口开发者可以自行扩展
      1. ConsumerBuilder enhanceProps(Consumer<Properties> fx)
        1. 如果开发者是开用 Properties 一步到位配置的,可以采用该钩子函数继续扩展
      2. ConsumerBuilder enhanceConfigs(Consumer<Map<String, Object>> fx)
        1. 如果开发者是采用 Builder 模式配置或者 configs(Map<String, Object> configs) 模式配置,可采用该钩子函数扩展
  2. 在获取 KafkaConsumer 实例时,配置需要订阅的 Topic
  3. 我们同样可以采用 checkXxx 钩子函数来对我们的配置进行校验
@Test
void testConsumer_with_subscribe() {
    KafkaEngine kafkaEngine = this.kafkaEngine();

    // 1.获取 KafkaConsumer 实例
    // 2.在获取 KafkaConsumer 实例时,配置需要订阅的 Topic
    try (KafkaConsumer<String, String> consumer = kafkaEngine.consumerService().createConsumer()
         .boostrapServers(this.defaultBoostrapServers())
         .keyDeserializer(StringDeserializer.class)
         .valueDeserializer(StringDeserializer.class)
         .autoOffsetReset(Kafka.Consumer.AutoOffsetReset.EARLIEST)
         .groupId(this.defaultGroup())
         .autoCommit(true)
         // 配置需要订阅的 Topic
         .subscribe(this.defaultTopic())
         .checkConfigs(super::testBoostrapServers)
         .build()) {

        Assertions.assertNotNull(consumer);
    }
}

2.5.Producer

Producer 的设计思想和使用方式大致和 Consumer 类似,做到一致性。

  • 配置键值对的序列化器
  • 框架考虑到在实际开发过程中可能会使用到用对象作为消息体载体,于是基于 Jackson 设计了
    • JacksonSerializer
    • JacksonDeserializer
@Test
void testProducer() {
    StringSerializer keySerializer = new StringSerializer();
    StringSerializer valueSerializer = new StringSerializer();

    KafkaEngine kafkaEngine = this.kafkaEngine();

    try (KafkaProducer<String, String> producer = kafkaEngine.producerService().createProducer()
         .boostrapServers(this.defaultBoostrapServers())
         .keySerializer(keySerializer)
         .valueSerializer(valueSerializer)
         .build()) {

        Assertions.assertNotNull(producer);
    }
}

2.6.ProducerRecord

使用方式依然和前序的风格一致。

@Test
void testProducerRecord() {
    KafkaEngine kafkaEngine = this.kafkaEngine();

    ProducerRecord<String, String> record = kafkaEngine.producerService().createProducerRecord()
        .topic(this.defaultTopic())
        .key("key-9527")
        .value("value-9527")
        .build();

    Assertions.assertNotNull(record);
}

3.Next

  • 基于 KafkaPlusProperties 实现 ConsumerProducer 的自动配置

4.思考中

接下来 KP 将会考虑是否对 Kafka 的消费端再次进行增强。

  • 支持类似于 @KafkaListener 的注解模式
  • 在合适的场景下开启并发消费,提升系统的消费能力
  • 期待未来 KRaft4.x 中正式启用时我们再次出发

至此,KP 也大致讲的差不多了,如果感兴趣那就赶快 Run 起来吧~

4.总结

通过 API 的抽象设计,让松散和繁杂的事情简单化。

Less is More