webFlux自定义多kafka监听

架包

xml 复制代码
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor.kafka</groupId>
            <artifactId>reactor-kafka</artifactId>
            <version>1.3.11</version>
        </dependency>

配置项

yaml 复制代码
kafka:
  enabled:
    listen: #监听器是否启用
      root: ${KAFKA_ENABLED_LISTEN_ROOT:false} #kafka是否启用监听的总开关
      consumer1: ${KAFKA_ENABLED_LISTEN_CONSUMER_1:true} #kafka是否启用监听器1的开关
      consumer2: ${KAFKA_ENABLED_LISTEN_CONSUMER_2:true} #kafka是否启用监听器2的开关
  reactive: #{@link com.kittlen.boot.distribute.kafka.config.properties.ReactiveKafkaConsumerProperties}
    #    配置消费者信息
    consumer1: #第一个消费者
      canConsume: ${KAFKA_CONSUMER_1_CAN_CONSUME:true} #是否消费消息
      bootstrap-servers:  ${KAFKA_C1_CONSUMER_ADDR:192.168.1.52:9092} #kafka服务地址
      #      bootstrap-servers:  ${KAFKA_CONSUMER_1_CONSUMER_ADDR:127.0.0.1:9092} #kafka服务地址
      key-serializer: org.apache.kafka.common.serialization.ByteArraySerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      max-poll-records: ${KAFKA_CONSUMER_1_CONSUMER_MAX_POLL_RECORDS:100}
      auto-offset-reset: ${KAFKA_CONSUMER_1_CONSUMER_AUTO_OFFSET_RESET:latest} #earliest 最早未被消费的offset ; latest 消费最新的offset
      enable-auto-commit: false
      group-id: ${KAFKA_CONSUMER_1_CONSUMER_GROUP_ID:mmcd}
      topics: ${KAFKA_CONSUMER_1_CONSUMER_TOPIC:t1}
    consumer2: #消费者2
      canConsume: ${KAFKA_CONSUMER_2_CAN_CONSUME:true} #是否消费消息
      properties:
        security:
          protocol: SASL_PLAINTEXT
        sasl:
          mechanism: SCRAM-SHA-256
          jaas:
            config: 'org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_CONSUMER_2_CONSUMER_USERNAME:user}" password="${KAFKA_CONSUMER_2_CONSUMER_PASSWORD:pwd}";'
      bootstrap-servers:  ${KAFKA_CONSUMER_2_CONSUMER_ADDR:192.168.1.55:9092} #kafka服务地址
      key-serializer: org.apache.kafka.common.serialization.ByteArraySerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      max-poll-records: ${KAFKA_CONSUMER_1_CONSUMER_MAX_POLL_RECORDS:100}
      auto-offset-reset: ${KAFKA_CONSUMER_2_CONSUMER_AUTO_OFFSET_RESET:latest}
      group-id: ${KAFKA_CONSUMER_2_CONSUMER_GROUP_ID:mmcd}
      topics: ${KAFKA_CONSUMER_2_CONSUMER_TOPIC:t2}

代码

配置配置项实体

java 复制代码
package com.kittlen.boot.distribute.kafka.ext;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;

/**
 * @author kittlen
 * @version 1.0
 */
@Getter
@Setter
@ApiModel("kafka消费者")
public class KafkaConsumerExt extends KafkaProperties.Consumer {

    /**
     * 监听的topic,多个用逗号隔开
     */
    @ApiModelProperty("监听的topic,多个用逗号隔开")
    private String topics;

    /**
     * 是否消费消息
     */
    @ApiModelProperty("是否消费消息")
    private boolean canConsume = true;
}
java 复制代码
package com.kittlen.boot.distribute.kafka.config.properties;

import com.kittlen.boot.distribute.kafka.ext.KafkaConsumerExt;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author kittlen
 * @version 1.0
 */
@Getter
@Setter
@Component
@ApiModel("kafka消费者配置参数")
@ConfigurationProperties(ReactiveKafkaConsumerProperties.PROPERTIES)
public class ReactiveKafkaConsumerProperties {

    public static final String PROPERTIES = "kafka.reactive";

    /**
     * 消费者1参数
     */
    @ApiModelProperty("消费者1参数")
    private KafkaConsumerExt consumer1 = new KafkaConsumerExt();

    /**
     * 消费者2参数
     */
    @ApiModelProperty("消费者2参数")
    private KafkaConsumerExt consumer2 = new KafkaConsumerExt();
}

config

java 复制代码
package com.kittlen.boot.distribute.kafka.config;

import com.kittlen.boot.distribute.kafka.config.properties.ReactiveKafkaConsumerProperties;
import com.kittlen.boot.distribute.kafka.ext.KafkaConsumerExt;
import com.kittlen.boot.distribute.kafka.ext.ReactiveKafkaConsumerTemplateExt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import reactor.kafka.receiver.ReceiverOptions;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author kittlen
 * @version 1.0
 */
@Slf4j
@Component
@ConditionalOnProperty(value = ReactiveConsumerConfig.CONSUMER_ROOT_ENABLED_PROPERTY, havingValue = "true")
public class ReactiveConsumerConfig {

    /**
     * 是否启用kafkaConsumer总开关
     */
    public static final String CONSUMER_ROOT_ENABLED_PROPERTY = "kafka.enabled.listen.root";
    /**
     * 是否启用kafkaConsumer1开关
     */
    public static final String CONSUMER_CONSUMER1_ENABLED_PROPERTY = "kafka.enabled.listen.consumer1";
    /**
     * 是否启用kafkaConsumer2开关
     */
    public static final String CONSUMER_CONSUMER2_ENABLED_PROPERTY = "kafka.enabled.listen.consumer2";

    /**
     * 监听器1
     *
     * @param reactiveKafkaConsumerProperties
     * @return
     */
    @Bean
    @ConditionalOnProperty(value = ReactiveConsumerConfig.CONSUMER_CONSUMER1_ENABLED_PROPERTY, havingValue = "true")
    public ReactiveKafkaConsumerTemplateExt<String, String> consumer1ReactiveKafkaConsumerTemplate(ReactiveKafkaConsumerProperties reactiveKafkaConsumerProperties) {
        KafkaConsumerExt consumer1 = reactiveKafkaConsumerProperties.getConsumer1();
        List<String> topics = Stream.of(consumer1.getTopics().split(",")).collect(Collectors.toList());
        ReceiverOptions<String, String> objectObjectReceiverOptions = ReceiverOptions.create(consumer1.buildProperties());
        ReceiverOptions<String, String> subscription = objectObjectReceiverOptions.subscription(topics);
        ReactiveKafkaConsumerTemplateExt<String, String> stringStringReactiveKafkaConsumerTemplate = new ReactiveKafkaConsumerTemplateExt<>(subscription, consumer1, "consumer1");
        log.info("loading consumer1 reactive kafka consumer completed");
        return stringStringReactiveKafkaConsumerTemplate;
    }

    /**
     * 监听器2
     *
     * @param reactiveKafkaConsumerProperties
     * @return
     */
    @Bean
    @ConditionalOnProperty(value = ReactiveConsumerConfig.CONSUMER_CONSUMER2_ENABLED_PROPERTY, havingValue = "true")
    public ReactiveKafkaConsumerTemplateExt<String, String> consumer2ReactiveKafkaConsumerTemplate(ReactiveKafkaConsumerProperties reactiveKafkaConsumerProperties) {
        KafkaConsumerExt consumer2= reactiveKafkaConsumerProperties.getConsumer2();
        List<String> topics = Stream.of(consumer2.getTopics().split(",")).collect(Collectors.toList());
        ReceiverOptions<String, String> objectObjectReceiverOptions = ReceiverOptions.create(consumer2.buildProperties());
        ReceiverOptions<String, String> subscription = objectObjectReceiverOptions.subscription(topics);
        ReactiveKafkaConsumerTemplateExt<String, String> stringStringReactiveKafkaConsumerTemplate = new ReactiveKafkaConsumerTemplateExt<>(subscription, consumer2, "consumer2");
        log.info("loading consumer2 reactive kafka consumer completed");
        return stringStringReactiveKafkaConsumerTemplate;
    }

}
java 复制代码
package com.kittlen.boot.distribute.kafka.consumer;

import com.kittlen.boot.comm.exceptions.BizException;
import com.kittlen.boot.distribute.kafka.config.ReactiveConsumerConfig;
import com.kittlen.boot.distribute.kafka.consumer.handlers.AbstractConsumerHandler;
import com.kittlen.boot.distribute.kafka.ext.ReactiveKafkaConsumerTemplateExt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author kittlen
 * @version 1.0
 */
@Slf4j
@Component
@ConditionalOnBean(ReactiveConsumerConfig.class)
public class KafkaReactiveConsumer {

    private final Map<String, AbstractConsumerHandler> handlerMap;
    private final Duration defTimeOut = Duration.ofSeconds(240);

    public KafkaReactiveConsumer(List<ReactiveKafkaConsumerTemplateExt<String, String>> reactiveKafkaConsumerTemplateExtList, List<AbstractConsumerHandler> consumerHandlers) {
        handlerMap = consumerHandlers.stream().collect(Collectors.toMap(AbstractConsumerHandler::consumerName, a -> a, (a, b) -> {
            throw new BizException("consumerName: " + b.consumerName() + " 重复");
        }));
        for (ReactiveKafkaConsumerTemplateExt<String, String> stringStringReactiveKafkaConsumerTemplateExt : reactiveKafkaConsumerTemplateExtList) {
            this.consumerRun(stringStringReactiveKafkaConsumerTemplateExt);
        }
    }

    private void consumerRun(ReactiveKafkaConsumerTemplateExt<String, String> reactiveKafkaConsumerTemplateExt) {
        if (reactiveKafkaConsumerTemplateExt == null) {
            return;
        }
        log.info("consumer:{}监听services:{},topic:{}", reactiveKafkaConsumerTemplateExt.getTemplateConsumerName(), reactiveKafkaConsumerTemplateExt.getKafkaConsumerExt().getBootstrapServers(), reactiveKafkaConsumerTemplateExt.getKafkaConsumerExt().getTopics());
        reactiveKafkaConsumerTemplateExt.receiveAutoAck().map(c -> {
            String templateConsumerName = reactiveKafkaConsumerTemplateExt.getTemplateConsumerName();
            if (!reactiveKafkaConsumerTemplateExt.isCanConsume()) {
                log.debug("consumer:{} not consume topic:{} value:{}", templateConsumerName, c.topic(), c.value());
                return Mono.empty();
            }
            log.debug("consumer:{} handler consume topic:{} value:{}", templateConsumerName, c.topic(), c.value());
            AbstractConsumerHandler abstractConsumerHandler = handlerMap.get(templateConsumerName);
            try {
                if (abstractConsumerHandler != null) {
                    return abstractConsumerHandler.handler(c).timeout(defTimeOut, Mono.just(false));
                } else {
                    log.info("未知consumerHandler:{}", templateConsumerName);
                    return Mono.empty();
                }
            } catch (Exception e) {
                log.error("consumer:{} 处理topic:{}实体信息:{}时出现异常为:{}", templateConsumerName, c.topic(), c.value(), e.getMessage(), e);
                return Mono.empty();
            }
        }).doOnError(throwable -> log.error("something bad happened while consuming : {}", throwable.getMessage()))
                .subscribe(Mono::subscribe);
    }

}

消息处理

通过实现下列接口来处理消息

java 复制代码
package com.kittlen.boot.distribute.kafka.consumer.handlers;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import reactor.core.publisher.Mono;

/**
 * @author kittlen
 * @version 1.0
 */
public interface AbstractConsumerHandler {

    /**
     * 处理器名
     * 与监听器的templateConsumerName一直,次处理器就会使用
     *
     * @return
     */
    String consumerName();

    /**
     * 处理监听器消息
     * 处理对应consumerName监听器监听到的信息
     *
     * @return
     */
    Mono<Boolean> handler(ConsumerRecord<String, String> consumerRecord);
}
相关推荐
江畔柳前堤5 小时前
云原生 × AI 全景图谱:从 Kubernetes 到 Agent 基础设施的一次认知跃迁
人工智能·分布式·gpt·云原生·kubernetes·原型模式·agi
Thomas21438 小时前
kafka 存储flink changelog两种方式和形态
分布式·flink·kafka
GIR1238 小时前
首发:2026年分布式储能系统市场分析报告——覆盖发展前景、占有率与十五五规划
分布式
leo_messi949 小时前
面试知识点梳理及相关面试题(十六)-- 分布式设计
分布式·面试·职场和发展
IT大白鼠19 小时前
MySQL 分布式集群系列 · 第四篇——实操部署指南:从零搭建生产级 MySQL NDB 集群
数据库·分布式·mysql
IT大白鼠20 小时前
MySQL 分布式集群系列 · 第三篇——核心原理精讲:NDB 自动分片、多主写入与数据同步
数据库·分布式·mysql
禾吾1 天前
Kafka基础-安装、基础命令等
kafka
cpolar技术支持1 天前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams
星恒讯工业路由器1 天前
分布式光伏站点分散难联网?4G/5G点对多点自组网方案解析
分布式·物联网·5g·分布式光伏·自组网·点对多点通信·4g/5g
qq_452396231 天前
第十一篇:《eBPF 进阶:自定义探针与生产级可观测性部署》
分布式