【问题标题】:Spring Kafka: Close the container and read the messages from specific offset with ConcurrentKafkaListenerContainerFactorySpring Kafka:关闭容器并使用 ConcurrentKafkaListenerContainerFactory 从特定偏移量读取消息
【发布时间】:2021-11-08 15:22:22
【问题描述】:

在我的 spring kafka 应用程序中,我想根据一些调度程序的输入在运行时触发消费者。调度程序将告诉侦听器它可以从哪个主题开始消费消息。有自定义 ConcurrentKafkaListenerContainerFactory 类的 springboot 应用程序。我需要执行三项任务:

  1. 关闭容器,成功读取主题上所有可用消息后。
  2. 它将当前偏移量存储在数据库或文件系统中。
  3. 下次消费者再次启动时,存储的偏移量可用于处理记录,而不是Kafka管理的默认偏移量。这样将来我们可以更改 DB 中的偏移值并获得所需的报告。 我知道如何使用@KafkaListener 处理所有这些问题,但不确定如何使用ConcurrentKafkaListenerContainerFactory。当前代码如下:
@SpringBootApplication
public class KafkaApp{


    public static void main(String[] args) {
        SpringApplication.run(KafkaApp.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("testTopic").partitions(1).replicas(1).build();
    }

     }

     @Component
     class Listener {

    private static final Logger log = LoggerFactory.getLogger(Listener.class);

    private static final Method otherListen;

    static {
        try {
            otherListen = Listener.class.getDeclaredMethod("otherListen", List.class);
        }
        catch (NoSuchMethodException | SecurityException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private final ConcurrentKafkaListenerContainerFactory<String, String> factory;

    private final MessageHandlerMethodFactory methodFactory;

    private final KafkaAdmin admin;

    private final KafkaTemplate<String, String> template;

    public Listener(ConcurrentKafkaListenerContainerFactory<String, String> factory, KafkaAdmin admin,
            KafkaTemplate<String, String> template, KafkaListenerAnnotationBeanPostProcessor<?, ?> bpp) {

        this.factory = factory;
        this.admin = admin;
        this.template = template;
        this.methodFactory = bpp.getMessageHandlerMethodFactory();
    }

    @KafkaListener(id = "myId", topics = "testTopic")
    public void listen(String topicName) {
        try (AdminClient client = AdminClient.create(this.admin.getConfigurationProperties())) {
            NewTopic topic = TopicBuilder.name(topicName).build();
            client.createTopics(List.of(topic)).all().get(10, TimeUnit.SECONDS);
        }
        catch (Exception e) {
            log.error("Failed to create topic", e);
        }
        ConcurrentMessageListenerContainer<String, String> container =
                this.factory.createContainer(new TopicPartitionOffset(topicName, 0));
        BatchMessagingMessageListenerAdapter<String, String> adapter =
                new BatchMessagingMessageListenerAdapter<>(this, otherListen);
        adapter.setHandlerMethod(new HandlerAdapter(
                this.methodFactory.createInvocableHandlerMethod(this, otherListen)));
        FilteringBatchMessageListenerAdapter<String, String> filtered =
                new FilteringBatchMessageListenerAdapter<>(adapter, record -> !record.key().equals("foo"));
        container.getContainerProperties().setMessageListener(filtered);
        container.getContainerProperties().setGroupId("group.for." + topicName);
        container.setBeanName(topicName + ".container");
        container.start();
        IntStream.range(0, 10).forEach(i -> this.template.send(topicName, 0, i % 2 == 0 ? "foo" : "bar", "test" + i));
    }

    void otherListen(List<String> others) {
        log.info("Others: {}", others);
    }

}

编辑

@SpringBootApplication
public class KafkaApp{


    public static void main(String[] args) {
        SpringApplication.run(KafkaApp.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("testTopic").partitions(1).replicas(1).build();
    }

     }

     @Component
     class Listener {

    private static final Logger log = LoggerFactory.getLogger(Listener.class);

    private static final Method otherListen;

    static {
        try {
            otherListen = Listener.class.getDeclaredMethod("otherListen", List.class);
        }
        catch (NoSuchMethodException | SecurityException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private final ConcurrentKafkaListenerContainerFactory<String, String> factory;

    private final MessageHandlerMethodFactory methodFactory;

    private final KafkaAdmin admin;

    private final KafkaTemplate<String, String> template;

    public Listener(ConcurrentKafkaListenerContainerFactory<String, String> factory, KafkaAdmin admin,
            KafkaTemplate<String, String> template, KafkaListenerAnnotationBeanPostProcessor<?, ?> bpp) {

        this.factory = factory;
        this.admin = admin;
        this.template = template;
        this.methodFactory = bpp.getMessageHandlerMethodFactory();
    }

    @KafkaListener(id = "myId", topics = "testTopic")
    public void listen(String topicName) {
        try (AdminClient client = AdminClient.create(this.admin.getConfigurationProperties())) {
            NewTopic topic = TopicBuilder.name(topicName).build();
            client.createTopics(List.of(topic)).all().get(10, TimeUnit.SECONDS);
        }
        catch (Exception e) {
            log.error("Failed to create topic", e);
        }
        ConcurrentMessageListenerContainer<String, String> container =
                this.factory.createContainer(new TopicPartitionOffset(topicName, 0));
        BatchMessagingMessageListenerAdapter<String, String> adapter =
                new BatchMessagingMessageListenerAdapter<>(this, otherListen);
        adapter.setHandlerMethod(new HandlerAdapter(
                this.methodFactory.createInvocableHandlerMethod(this, otherListen)));
        FilteringBatchMessageListenerAdapter<String, String> filtered =
                new FilteringBatchMessageListenerAdapter<>(adapter, record -> !record.key().equals("foo"));
        container.getContainerProperties().setMessageListener(filtered);
        container.getContainerProperties().setGroupId("group.for." + topicName);
        container.setBeanName(topicName + ".container");
        container.getContainerProperties().setIdleEventInterval(3000L); 
        container.start();
        IntStream.range(0, 10).forEach(i -> this.template.send(topicName, 0, i % 2 == 0 ? "foo" : "bar", "test" + i));
    }

    void otherListen(List<String> others) {
        log.info("Others: {}", others);
    }
     @EventListener
    public void eventHandler(ListenerContainerIdleEvent event) {
        logger.info("No messages received for " + event.getIdleTime() + " milliseconds");
    }


}

【问题讨论】:

    标签: java spring-boot apache-kafka spring-kafka


    【解决方案1】:

    当没有消息需要处理时,您可以收到ListenerContainerIdleEvents;您可以使用此事件来停止容器;您应该在不同的线程(不是发布事件的线程)上执行stop()

    How to check if Kafka is empty using Spring Kafka?

    您可以通过多种方式获取分区/偏移量。

    void otherListen<List<ConsumerRecord<..., ...>>) 
    

    void otherListen(List<String> others,
        @Header(KafkaHeaders.RECEIVED_PARTITION) List<Integer> partitions,
        @Header(KafkaHeaders.OFFSET) List<Long> offsets)
    

    您可以在

    中指定起始偏移量
    new TopicPartitionOffset(topicName, 0), startOffset);
    

    在创建容器时。

    编辑

    要在容器空闲时停止容器,设置idleEventInterval 并添加@EventListener 方法并停止容器。

    TaskExecutor exec = new SimpleAsyncTaskExecutor();
    
    @EventListener
    void idle(ListenerContainerIdleEvent event) {
        log...
        this.exec.execute(() -> event.getContainer(ConcurrentMessageListenerContainer.class).stop());
    }
    

    如果您将concurrency 添加到您的容器中,您需要在停止父容器之前让每个子容器处于空闲状态。

    EDIT2

    我刚刚将它添加到我为回答您的其他问题而编写的代码中,它完全按预期工作。

        @KafkaListener(id = "so69134055", topics = "so69134055")
        public void listen(String topicName) {
            try (AdminClient client = AdminClient.create(this.admin.getConfigurationProperties())) {
                NewTopic topic = TopicBuilder.name(topicName).build();
                client.createTopics(List.of(topic)).all().get(10, TimeUnit.SECONDS);
            }
            catch (Exception e) {
                log.error("Failed to create topic", e);
            }
            ConcurrentMessageListenerContainer<String, String> container =
                    this.factory.createContainer(new TopicPartitionOffset(topicName, 0));
            BatchMessagingMessageListenerAdapter<String, String> adapter =
                    new BatchMessagingMessageListenerAdapter<>(this, otherListen);
            adapter.setHandlerMethod(new HandlerAdapter(
                    this.methodFactory.createInvocableHandlerMethod(this, otherListen)));
            FilteringBatchMessageListenerAdapter<String, String> filtered =
                    new FilteringBatchMessageListenerAdapter<>(adapter, record -> !record.key().equals("foo"));
            container.getContainerProperties().setMessageListener(filtered);
            container.getContainerProperties().setGroupId("group.for." + topicName);
            container.getContainerProperties().setIdleEventInterval(3000L);
            container.setBeanName(topicName + ".container");
            container.start();
            IntStream.range(0, 10).forEach(i -> this.template.send(topicName, 0, i % 2 == 0 ? "foo" : "bar", "test" + i));
        }
    
        void otherListen(List<String> others) {
            log.info("Others: {}", others);
        }
    
        TaskExecutor exec = new SimpleAsyncTaskExecutor();
    
        @EventListener
        public void idle(ListenerContainerIdleEvent event) {
            log.info(event.toString());
            this.exec.execute(() -> {
                ConcurrentMessageListenerContainer container = event.getContainer(ConcurrentMessageListenerContainer.class);
                log.info("stopping container: " + container.getBeanName());
                container.stop();
            });
        }
    
    [foo.container-0-C-1] Others: [test0, test2, test4, test6, test8]
    [foo.container-0-C-1] ListenerContainerIdleEvent [idleTime=5.007s, listenerId=foo.container-0, container=KafkaMessageListenerContainer [id=foo.container-0, clientIndex=-0, topicPartitions=[foo-0]], paused=false, topicPartitions=[foo-0]]
    [SimpleAsyncTaskExecutor-1] stopping container: foo.container
    [foo.container-0-C-1] [Consumer clientId=consumer-group.for.foo-2, groupId=group.for.foo] Unsubscribed all topics or patterns and assigned partitions
    [foo.container-0-C-1] Metrics scheduler closed
    [foo.container-0-C-1] Closing reporter org.apache.kafka.common.metrics.JmxReporter
    [foo.container-0-C-1] Metrics reporters closed
    [foo.container-0-C-1] App info kafka.consumer for consumer-group.for.foo-2 unregistered
    [foo.container-0-C-1] group.for.foo: Consumer stopped
    

    【讨论】:

    • 感谢@Gary,我已经编辑了上面的代码以合并以关闭容器,但它似乎不起作用。我已按照您的建议添加了“setIdleEventInterval”和 EventListener。
    • 除了登录事件处理程序之外,您没有做任何事情;您需要停止容器(异步以避免延迟)。
    • 谢谢@Gary,我的意思是说用'EventListener'注释的方法没有被调用。我无法看到记录器,甚至调试器在等待空闲时间后也不会继续使用该方法。请帮忙
    • 仅供参考:我使用的是 Kafka 版本:2.7.1
    • 我刚刚将它添加到我为你的另一个问题编写的代码中,它对我来说很好,所以你的代码有问题。请参阅第二次编辑。
    猜你喜欢
    • 2014-09-22
    • 2021-05-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-11
    • 2017-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多