Kafka与SpringBoot集成

  • 一、SpringBoot整合Kafka
  • 1.1 pom.xml
  • 1.2 application.yml
  • 1.3 消息发送
  • 1.3.1 发送消息
  • 1.3.2 异步消息发送监听
  • 1.3.3 序列化
  • 1. Serializer
  • 2. Deserializer
  • 1.3.4 分区器
  • 1. 默认分区器
  • 2. 自定义分区器
  • 1.4 消息接收
  • 1.4.1 @KafkaListener 注解
  • 1.4.2 MessageListener 接口
  • 1.4.3 消息组
  • 1.4.4 offset 提交
  • 1. 自动提交
  • 2. 手动提交
  • 二、@KafkaListener 注解
  • 2.1 KafkaListener 注解的主要属性
  • 2.2 KafkaListener注解样例
  • 三、MessageListener 接口
  • 3.1 消息监听容器
  • 3.2 消息监听容器工厂
  • 3.3 实现一个接口式消息监听器
  • 四、消息拦截器
  • 4.1 自定义消息拦截器
  • 4.2 添加配置
  • 4.2.1 yaml 配置
  • 4.2.2 代码初始化指定
  • 4.3 拦截器执行顺序


一、SpringBoot整合Kafka

1.1 pom.xml

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

1.2 application.yml

spring:
  application:
    name: spring-kafka

  kafka:
    # kafka集群地址,多个逗号隔开
    bootstrap-servers: 127.0.0.01:9092
    # producer 生产者
    producer:
      retries: 0 # 消息发送失败重试
      acks: 1 # 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
      batch-size: 16384 # 批量大小
      buffer-memory: 33554432 # 生产端缓冲区大小
      # key value 序列化机制
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

    # consumer消费者
    consumer:
      # 默认的消费组ID
      group-id: kafka-default-group
      # 是否自动提交 offset
      enable-auto-commit: true
      # 提交offset延时(接收到消息后多久提交offset)
      auto-commit-interval: 100
      # earliest: 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
      # latest: 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
      # none: topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
      auto-offset-reset: latest
      # key value 序列化机制
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

1.3 消息发送

Kafka 消息发送流程如下:

kafka整合spring boot kafka集成spring boot_kafka整合spring boot

1.3.1 发送消息

通过 KafkaTemplate 发送消息。

@RestController
@Slf4j
public class KafkaProducerController {
    @Autowired
    private KafkaTemplate kafkaTemplate;

    public static final String TOPIC = "spring-kafka-topic-test";

    @GetMapping("/sendMessage")
    public String sendMessage(String msg) throws Exception {
        // 默认异步发送消息
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(TOPIC, msg);
        // 通过 ListenableFuture.get() 同步等待
        SendResult sendResult = future.get();
        return sendResult.toString();
    }
}

1.3.2 异步消息发送监听

spring-kafka 发送消息采用异步方式,我们可以通过 ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(TOPIC, msg); SendResult sendResult = future.get(); 进行同步等待获取结果,也可以为 KafkaTemplate 添加一个异步消息发送监听器,来判断消息是否发送成功,如下:

@Configuration
@Slf4j
public class KafkaConfiguration<K, V> {
	@Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public KafkaTemplate<K, V> kvKafkaTemplate() {
        Map<String, Object> props = new HashMap<>();
        // kafka服务地址
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        // key、value 采用的序列化机制
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 添加自定义分区器
        // props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartition.class);
        KafkaTemplate<K, V> kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(props));
        kafkaTemplate.setProducerListener(new ProducerListener<K, V>() {
            @Override
            public void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata) {
                log.info("onSuccess record={}", producerRecord.value());
            }

            @Override
            public void onError(ProducerRecord<K, V> producerRecord, Exception exception) {
                log.info("onError record={}", producerRecord.value());
            }
        });
        return kafkaTemplate;
    }
}

1.3.3 序列化

spring-kafka 使用 KafkaTemplate 发送消息时,会指定序列化机制,如:

spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    # producer 生产者
    producer:
      # key value 序列化机制
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    # consumer消费者
    consumer:
      # key value 序列化机制
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

org.apache.kafka.common.serialization.StringSerializer 为 Kafka 自带的字符串序列化机制,除此之外,还提供很多默认序列化机制,如:ByteArray、ByteBuffer、Bytes、Double、Integer、Long 等,这些序列化器都实现了接口 org.apache.kafka.common.serialization.Serializer,因此,我们也可以实现自定义序列化机制,如下:

1. Serializer
public class MySerializer<T> implements Serializer<Object> {
    @Override
    public void configure(Map map, boolean b) {
        // 配置
    }

    @Override
    public byte[] serialize(String s, Object o) {
        // 序列化
        return JSON.toJSONString(o).getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public void close() {
        // 关闭时调用
    }
}
2. Deserializer
public class MyDeserializer<T> implements Deserializer<Object> {
    @Override
    public void configure(Map<String, ?> map, boolean b) {
        // 配置
    }

    @Override
    public Object deserialize(String s, byte[] bytes) {
        // 反序列化
        return JSON.parse(new String(bytes, StandardCharsets.UTF_8));
    }

    @Override
    public void close() {
        // 关闭时调用
    }
}

1.3.4 分区器

Kafka 分区器决定了消息根据 key 投放到哪个分区,也是顺序消费保障的基石。

  • 指定分区号,直接将数据发送到指定的分区
  • 没有指定分区号,key != null,通过给定数据的 key 进行 hashCode 取模判断
  • 既没有指定分区号,key == null,轮循发送(默认)
  • 自定义分区
1. 默认分区器
public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    public void configure(Map<String, ?> configs) {}

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (keyBytes == null) {
            int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                // no partitions are available, give a non-available partition
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    private int nextValue(String topic) {
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    public void close() {}
}
2. 自定义分区器

实现 org.apache.kafka.clients.producer.Partitioner 接口,重写 partition() 方法,如下:

public class MyPartition implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        /**
         * 自定义分区器:key 的哈希值取模
         */
        if (key == null) {
            return 0;
        }
        return key.toString().hashCode() % 2;
    }

    @Override
    public void close() {
        // 关闭时调用
    }

    @Override
    public void configure(Map<String, ?> map) {
        // 配置
    }
}

1.4 消息接收

kafka的消息监听有两种方式:

  1. 注解式监听
  2. 接口式监听

1.4.1 @KafkaListener 注解

@Component
@Slf4j
public class SpringKafkaTestConsumer {

    @KafkaListener(topics = KafkaProducerController.TOPIC)
    /*@KafkaListeners({
            // 可同时监听多个topic
            @KafkaListener(topics = KafkaProducerController.TOPIC)
    })*/
    public void onMessage(ConsumerRecord<String, String> record) {
        String value = record.value();
        System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
    }
}

关于 @KafkaListener 注解详解,参照下面。

1.4.2 MessageListener 接口

@Component
@Slf4j
public class SpringKafkaTestConsumer implements GenericMessageListener {
    @Override
    public void onMessage(Object o) {
        String value = (String) o;
        System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
    }

    @Override
    public void onMessage(Object data, Acknowledgment acknowledgment) {
        String value = (String) data;
        System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
    }

    @Override
    public void onMessage(Object data, Acknowledgment acknowledgment, Consumer consumer) {
        String value = (String) data;
        System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
    }

    @Override
    public void onMessage(Object data, Consumer consumer) {
        String value = (String) data;
        System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
    }
}

使用 MessageListener 需添加配置类,详细参照下面。

1.4.3 消息组

Kafka 消费者使用一个消费组名称来进行标识,发布到 topic 中的每条记录被分配给订阅消费组中的一个消费者实例。消费者实例可以分布在多个进程中或者多个机器上。

  • 如果所有的消费者实例在同一消费组中,消息记录会负载平衡到每一个消费者实例。
  • 如果所有的消费者实例在不同的消费组中,每条消息记录会广播到所有的消费者进程。

kafka整合spring boot kafka集成spring boot_ide_02


同一个 Topic 可由不同消费者组进行消费,如下:

// 组A,消费者1
@KafkaListener(topics = {"topic-test"},groupId = "group-A")
public void onMessage1(ConsumerRecord<?, ?> record) {
    String value = record.value();
    System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
}

// 组A,消费者2
@KafkaListener(topics = {"topic-test"},groupId = "group-A")
public void onMessage2(ConsumerRecord<?, ?> record) {
    String value = record.value();
    System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
}

@KafkaListener(topics = {"topic-test"},groupId = "group-B")
public void onMessage3(ConsumerRecord<?, ?> record) {
    String value = record.value();
    System.out.println("=================收到topic【" + KafkaProducerController.TOPIC + "】消息:" + value);
}

1.4.4 offset 提交

1. 自动提交

在 spring-kafka配置中,存在两个配置,如下:

# 是否自动提交 offset
enable-auto-commit: true
# 提交offset延时(接收到消息后多久提交offset),单位毫秒
auto-commit-interval: 100
2. 手动提交

大多数场景中,业务需要手动控制偏移量的提交时机,比如确保消息严格消费后再提交,以防止丢失或重复等。
下面我们自己定义配置,覆盖上面的参数。

@Configuration
public class KafkaConfiguration {
    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean("manualKafkaListenerContainerFactory")
    public KafkaListenerContainerFactory<?> manualKafkaListenerContainerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 设置手动提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(props));

        /**
         * ack模式,针对 ENABLE_AUTO_COMMIT_CONFIG=false 时生效,有7中模式,如下
         * 1.RECORD:每处理一条commit一次
         * 2.BATCH(默认):每次poll的时候批量提交一次,频率取决于每次poll的调用频率
         * 3.TIME:每次间隔ackTime的时间去commit
         * 4.COUNT:累积达到ackCount次的ack去commit
         * 5.COUNT_TIME:ackTime或ackCount哪个条件先满足,就commit
         * 6.MANUAL:listener负责ack,但是背后也是批量上去
         * 7.MANUAL_IMMEDIATE:listner负责ack,每调用一次,就立即commit
         */
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        return factory;
    }

}

然后通过在消费端的Consumer来提交偏移量

Kafka 提供了两种提交偏移量方式:

  1. consumer.commitAsync():异步提交
  2. consumer.commitSync():同步提交.

commitSync() 方法提交最后一个偏移量。在成功提交或碰到无怯恢复的错误之前,commitSync()会一直重试,但是commitAsync()不会。一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。否则就会造成重复消费因此,在消费者关闭前一般会组合使用 commitAsync() 和 commitSync(),如下:

@KafkaListener(topics = "topic-test", groupId = "offset-group", 
containerFactory = "manualKafkaListenerContainerFactory" // 指定容器创建工程)
public void manualCommit(@Payload String message,
                         @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
                         @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
                         Consumer consumer,
                         Acknowledgment ack) {
    try {
        // 异步提交
        consumer.commitAsync();
        
        //do something.....
        
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    } finally {
        try {
        	// 同步提交
            consumer.commitSync();
        } finally {
            consumer.close();
        }
    }
}

二、@KafkaListener 注解

2.1 KafkaListener 注解的主要属性

  • id:监听器 id,消费者线程命名会使用当前值为前缀,在相同容器中的监听器ID不能重复
  • groupId:kafka 消费组 id
  • idlsGroup:是否用 id 作为 groupId,如果置为 false,并指定 groupId 时,消费组 ID 使用 groupId;如果置为 true,会使用监听器的 id 作为 groupId
  • topics:topics:指定要监听哪些 topic(与 topicPattern、topicPartitions 三选一)
  • topicPattern: 匹配 topic 进行监听(与 topics、topicPartitions 三选一),topicPattern 支持表达式,如:
// _ 匹配一个字符,* 匹配多个字符
@KafkaListener(topicPattern = "aaa_.*_.*_bb.*")
  • topicPartitions: 显式分区分配,可以为监听器配置明确的主题和分区(以及可选的初始偏移量)
// 监听topic1的0,1分区
// 监听topic2的第0分区, 并且第1分区从offset为100的开始消费
@KafkaListener(topicPartitions = { 
	@TopicPartition(topic = "topic1", partitions = { "0", "1" }),
	@TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
})
  • containerFactory:指定监听器容器工厂
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> batchFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    // 设置为批量消费,通过参数 ConsumerConfig.MAX_POLL_RECORDS_CONFIG 指定最大拉取数量
    factory.setBatchListener(true);
    return factory;
}

@Bean
public ConsumerFactory<String, String> consumerFactory() {
    return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
}
  • errorHandler: 监听异常处理器,配置 Spring IoC 中的 BeanName。实现KafkaListenerErrorHandler; 然后做一些异常处理。
@Component
public class KafkaDefaultListenerErrorHandler implements KafkaListenerErrorHandler {
    @Override
    public Object handleError(Message<?> message, ListenerExecutionFailedException exception) {
        return null;
    }
 
    @Override
    public Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer) {
        
        return null;
    }
}
  • beanRef:真实监听容器的 BeanName,需要在 BeanName前加 “__”
  • clientIdPrefix:消费者Id前缀
  • concurrency: 覆盖容器工厂 containerFactory 的并发配置,表示消费者的个数,一般和topic分区设置成一样

注意:从 2.2.4 版开始,可以直接在注解中指定 Kafka 使用者属性,这些属性将覆盖在使用者工厂中配置的具有相同名称的所有属性。可以使用 #{…} 或属性占位符(${…})在 SpEL 上配置注释上的大多数属性。如:
@KafkaListener(concurrency = "${listen.concurrency:3}") 标识属性 concurrency 将会从容器中获取 listen.concurrency 的值,如果不存在就使用默认值3

2.2 KafkaListener注解样例

@KafkaListener(topics = KafkaProducerController.TOPIC)
/*@KafkaListeners({
        // 可同时监听多个topic
        @KafkaListener(topics = KafkaProducerController.TOPIC)
})*/
public void onMessage(ConsumerRecord<String, String> record) {
    String value = record.value();
    System.out.println("=================收到topic消息:" + value);
}
@KafkaListener(topicPattern = "topic-test")
public void onMessage(@Payload String data,
                      @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
                      @Header(KafkaHeaders.RECEIVED_MESSAGE_KEY) ByteBuffer key,
                      Acknowledgment ack,// 手动提交offset
                      @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
                      @Header(KafkaHeaders.OFFSET) long offSet,
                      Consumer<?, ?> consumer) {
	try {
        String value= (String) record.value();
    	System.out.println("=================收到topic消息:" + value);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    } finally {
        // 手动提交 offset
        ack.acknowledge();
    }
}

三、MessageListener 接口

Kafka 的消息监听一般可以分为:1.单条数据监听;2.批量数据监听。GenericMessageListener 是 Spring Kafka 的消息监听器接口,也是一个函数式接口,利用接口的 onMessage() 方法可以实现消费数据,如下:

@FunctionalInterface
public interface GenericMessageListener<T> {

	void onMessage(T data);

	default void onMessage(T data, Acknowledgment acknowledgment) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	default void onMessage(T data, Consumer<?, ?> consumer) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	default void onMessage(T data, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
		throw new UnsupportedOperationException("Container should never call this");
	}
}

spring-kafka 根据 GenericMessageListener 默认提供的接口有:

  1. 单条数据消息监听器接口 MessageListenen
  2. 多条数据消息监听器接口 BatchMessageListener
  3. 带ACK机制的单条数据消息监听器 AcknowledgingMessageListener
  4. 带ACK机制的多条数据消息监听器 BatchAcknowledgingMessageListener

其中,类关系图如下:

kafka整合spring boot kafka集成spring boot_ide_03

3.1 消息监听容器

消息监听器 MessageListener 需要通过消费监听器容器 MessageListenerContainer 接口中的 setupMessageListenner() 方法来启动监听,如下:

public interface MessageListenerContainer extends SmartLifecycle {

	// 设置消息监听器
	void setupMessageListener(Object messageListener);

	// 返回按 client-id 分组的消费者指标,key -> client-id
	Map<String, Map<MetricName, ? extends Metric>> metrics();

	// 获取容器属性
	default ContainerProperties getContainerProperties() {
		throw new UnsupportedOperationException("This container doesn't support retrieving its properties");
	}

	// 获取容器的topic、partition
	default Collection<TopicPartition> getAssignedPartitions() {
		throw new UnsupportedOperationException("This container doesn't support retrieving its assigned partitions");
	}

	// 在下一个轮询之前暂停此容器。
	default void pause() {
		throw new UnsupportedOperationException("This container doesn't support pause");
	}

	// 如果暂停,则在下一个轮询之后恢复此容器。
	default void resume() {
		throw new UnsupportedOperationException("This container doesn't support resume");
	}

	// true if pause has been requested.
	default boolean isPauseRequested() {
		throw new UnsupportedOperationException("This container doesn't support pause/resume");
	}

	// true if the container is paused.
	default boolean isContainerPaused() {
		throw new UnsupportedOperationException("This container doesn't support pause/resume");
	}

	// 设置自动启动
	default void setAutoStartup(boolean autoStartup) {
		// empty
	}
}

spring-kafka 提供了两个容器 KafkaMessageListenerContainerConcurrentMessageListenerContainer

kafka整合spring boot kafka集成spring boot_java_04

3.2 消息监听容器工厂

消息监听器容器由容器工厂 KafkaListenerContainerFactory 统一管理和创建,如下,需指定消息监听容器类型。

public interface KafkaListenerContainerFactory<C extends MessageListenerContainer> {
	// 根据 endpoint 创建一个消息监听器容器
	C createListenerContainer(KafkaListenerEndpoint endpoint);

	// 根据 topic、partition 创建一个消息监听器容器
	C createContainer(Collection<TopicPartitionInitialOffset> topicPartitions);

	// 根据 topic 创建一个消息监听器容器
	C createContainer(String... topics);

	// 根据 topic 的正则表达式创建一个消息监听器容器
	C createContainer(Pattern topicPattern);
}

spring-kafka 提供了监听器容器工厂 ConcurrentKafkaListenerContainerFactory,类结构图如下:

kafka整合spring boot kafka集成spring boot_java_05


其有两个重要的配置

  1. ContainerProperties:ContainerProperties 定义了要消费消息的 topic,消息处理的 MessageListener 等信息。
  2. ConsumerFactory:ConsumerFactory 指定了消费者工厂

3.3 实现一个接口式消息监听器

  1. 创建一个 ConsumerFactory
  2. 创建 ConcurrentKafkaListenerContainerFactory 容器工厂
  3. 创建 KafkaMessageListenerContainer 监听容器

如下:

@Configuration
@Slf4j
public class KafkaConfiguration<K, V> {
    @Autowired
    private KafkaProperties kafkaProperties;

    @Bean
    public ConsumerFactory<K, V> consumerFactory() {
        // 通过 KafkaProperties ConsumerProperties 创建一个默认的 ConsumerFactory
        return new DefaultKafkaConsumerFactory(kafkaProperties.buildConsumerProperties());
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<K, V> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<K, V> factory = new ConcurrentKafkaListenerContainerFactory<>();
        // 深圳 ConsumerFactory
        factory.setConsumerFactory(consumerFactory());
        // 指定容器并发性:表示消费者的个数,一般和 topic 分区设置成一样
        factory.setConcurrency(3);
        // 是否为批量监听
        factory.setBatchListener(true);
        // 设置拉取超时时间,单位毫秒(ms)
        factory.getContainerProperties().setPollTimeout(3000);
        return factory;
    }

    @Bean
    public KafkaMessageListenerContainer<K, V> kafkaMessageListenerContainer() {
        // 创建一个容器属性对象:监听 spring-kafka-topic-test、spring-kafka-topic-test-1 消息
        ContainerProperties containerProperties = new ContainerProperties("spring-kafka-topic-test", "spring-kafka-topic-test-1");
        // 设置消息监听:自定义消息监听器
        containerProperties.setMessageListener((MessageListener<K, V>) data ->
                System.out.println("收到消息: " + data.value())
        );
        return new KafkaMessageListenerContainer<K, V>(consumerFactory(), containerProperties);
    }
}

四、消息拦截器

实际业务开发中,很多场景需要在消息发送前进行某些操作,如按照某个规则过滤掉不符合要求的消息,或者屏蔽某些关键词,或者改变某些属性等等,都可以依托消息拦截器链来实现。

自定义拦截器需要实现 org.apache.kafka.clients.producer.ProducerInterceptor 接口,根据需要重写相应的方法。

public interface ProducerInterceptor<K, V> extends Configurable {
	// 重新当前方法,可改变消息内容
    public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);

	// 如果需要对消息发送的结果进行处理,则需要重写onAcknowledgement方法,
	// exception有值,说明消息发送失败,可以采取自定义的措施,比如重试等等,也可以用来统计消息发送的成功率
    public void onAcknowledgement(RecordMetadata metadata, Exception exception);

	// Producer关闭时调用
    public void close();
}

4.1 自定义消息拦截器

@Slf4j
public class MyProducerInterceptor implements ProducerInterceptor {

    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        log.info("myProducerInterceptor>>>>>>>onSend record={}", JSON.toJSONString(record));

        String value = (String) record.value();
        value += "   timeMillis:" + System.currentTimeMillis();

        ProducerRecord<String, String> newRecord = new ProducerRecord<>(
                record.topic(),
                record.partition(),
                record.timestamp(),
                (String) record.key(),
                value,  // 修改后的 value值
                record.headers());
        return newRecord;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        log.info("myProducerInterceptor>>>>>>>onAcknowledgement metadata={}", JSON.toJSONString(metadata));
        if (exception != null) {
            // 发送失败
            log.info("myProducerInterceptor>>>>>>>onAcknowledgement exception={}", exception.getMessage());
        }
    }

    @Override
    public void close() {
        log.info("myProducerInterceptor>>>>>>>close");
    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

4.2 添加配置

4.2.1 yaml 配置

spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    # producer 生产者
    producer:
      retries: 0 # 消息发送失败重试
      acks: 1 # 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
      batch-size: 16384 # 批量大小
      buffer-memory: 33554432 # 生产端缓冲区大小
      # key value 序列化机制
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      properties:
        # 拦截器配置,多个用逗号隔开
        interceptor:
          classes: xxx.xxx.xxx.MyProducerInterceptor,xxx.xxx.xxx.My2ProducerInterceptor

4.2.2 代码初始化指定

@Configuration
@Slf4j
public class KafkaConfiguration<K, V> {
	@Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public KafkaTemplate<K, V> kafkaTemplate() {
        Map<String, Object> props = new HashMap<>();
        // kafka服务地址
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        // key、value 采用的序列化机制
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 添加自定义分区器
        // props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartition.class);
        // 自定义拦截器
        List<String> interceptors = Arrays.asList(MyProducerInterceptor.class.getName());
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
        
        KafkaTemplate<K, V> kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(props));
        return kafkaTemplate;
    }
}

4.3 拦截器执行顺序

执行顺序取决于配置的先后顺序,在前面的先执行。

同一个拦截器添加多次,会执行多次。