Spring Kafka深入学习分析

文章出处shenyifengtk.github.io 转载请注明

本文由来,有一个需求要在浏览器输入Kafka topic,消费组提交后自动开启消费,这个做起来比较简单,同事使用了Kafka 驱动包很快速完成这个。我突然想到能不能通过Spring Kafka自身框架完成这个功能,不使用底层驱动包来自做呢。而引出分析整个Spring Kafka 如何实现注解消费信息,调用方法的。并且最后通过几个简单的代码完成上面小需求。

源码解析

EnableKafka入口

kafka 模块的开始先从@EnableKafka 上@Import(KafkaListenerConfigurationSelector.class)

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] { KafkaBootstrapConfiguration.class.getName() };
    }

接着继续看下KafkaBootstrapConfiguration类

public class KafkaBootstrapConfiguration implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition(
                KafkaListenerConfigUtils.KAFKA_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)) {

            registry.registerBeanDefinition(KafkaListenerConfigUtils.KAFKA_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,
                    new RootBeanDefinition(KafkaListenerAnnotationBeanPostProcessor.class));
        }

        if (!registry.containsBeanDefinition(KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)) {
            registry.registerBeanDefinition(KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,
                    new RootBeanDefinition(KafkaListenerEndpointRegistry.class));
        }
    }

}

使用BeanDefinitionRegistry 将class 转换成beanDefinition,注册到beanDefinitionMap 容器中,容器会统一将Map Class全部进行实例化,其实就是将这个交给Spring 初始化。


image.png

KafkaListenerAnnotationBeanPostProcessor 解析

下面看下kafka核心处理类KafkaListenerAnnotationBeanPostProcessor 如何解析@KafkaListener 注解,postProcessAfterInitialization 在bean 实例化后调用方法,对bean 进行增强。

    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
            //如果此时bean可能是代理类,则获取原始class ,否则直接class
            Class<?> targetClass = AopUtils.getTargetClass(bean); 
           //这时类上去找@KafkaListener  ,因为在class 上可能出现多种复杂情况,这个方法封装一系列方法能包装找到注解
          //这里可能存在子父类同时使用注解,所有只有找到一个就进行对应方法处理
            Collection<KafkaListener> classLevelListeners = findListenerAnnotations(targetClass);
            final boolean hasClassLevelListeners = classLevelListeners.size() > 0;
            final List<Method> multiMethods = new ArrayList<>();
              //从方法上找注解,找到方法放到map中,Method 当作key
            Map<Method, Set<KafkaListener>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                    (MethodIntrospector.MetadataLookup<Set<KafkaListener>>) method -> {
                        Set<KafkaListener> listenerMethods = findListenerAnnotations(method);
                        return (!listenerMethods.isEmpty() ? listenerMethods : null);
                    });
            if (hasClassLevelListeners) { //如果类上有注解的话,都有搭配@KafkaHandler使用的,方法上找这个注解
                Set<Method> methodsWithHandler = MethodIntrospector.selectMethods(targetClass,
                        (ReflectionUtils.MethodFilter) method ->
                                AnnotationUtils.findAnnotation(method, KafkaHandler.class) != null);
                multiMethods.addAll(methodsWithHandler);
            }
            if (annotatedMethods.isEmpty()) { //将解析过class 缓存起来
                this.nonAnnotatedClasses.add(bean.getClass());
            else {
                // Non-empty set of methods
                for (Map.Entry<Method, Set<KafkaListener>> entry : annotatedMethods.entrySet()) {
                    Method method = entry.getKey();
                    for (KafkaListener listener : entry.getValue()) {
                        processKafkaListener(listener, method, bean, beanName);  //方法监听处理的逻辑
                    }
                }
                this.logger.debug(() -> annotatedMethods.size() + " @KafkaListener methods processed on bean '"
                            + beanName + "': " + annotatedMethods);
            }
            if (hasClassLevelListeners) {
                processMultiMethodListeners(classLevelListeners, multiMethods, bean, beanName); //KafkaHandler 处理逻辑
            }
        }
        return bean;
    }

@kafkaListener其实可以作用于Class 上的,搭配着@KafkaHandler一起使用,那怎么样使用呢,我用一个简单例子展示下。

@KafkaListener(topics = "${topic-name.lists}",groupId = "${group}",concurrency = 4)
public class Kddk {
    
    @KafkaHandler
    public void user(User user){
        
    }
    
    @KafkaHandler
    public void std(Dog dog){
        
    }
}

消费信息不同对象区分进行处理,省去对象转换的麻烦,我暂时想到场景就是这些,平常很少有这些。这个实现原理我就不深入分析了

    protected void processKafkaListener(KafkaListener kafkaListener, Method method, Object bean, String beanName) {
        //如果方法刚好被代理增强了,返回原始class 方法
        Method methodToUse = checkProxy(method, bean);
        MethodKafkaListenerEndpoint<K, V> endpoint = new MethodKafkaListenerEndpoint<>();
        endpoint.setMethod(methodToUse);

        String beanRef = kafkaListener.beanRef();
        this.listenerScope.addListener(beanRef, bean);
        String[] topics = resolveTopics(kafkaListener);
        TopicPartitionOffset[] tps = resolveTopicPartitions(kafkaListener);
         //这个方法是判断方法上是否有@RetryableTopic 注解,有的话则放回true,注册到KafkaListenerEndpointRegistry
        if (!processMainAndRetryListeners(kafkaListener, bean, beanName, methodToUse, endpoint, topics, tps)) {
            //解析@kafkaListener 属性,设置到endpoint ,注册到KafkaListenerEndpointRegistry
            processListener(endpoint, kafkaListener, bean, beanName, topics, tps); 
        }
        this.listenerScope.removeListener(beanRef);
    }

    protected void processListener(MethodKafkaListenerEndpoint<?, ?> endpoint, KafkaListener kafkaListener,
                                Object bean, String beanName, String[] topics, TopicPartitionOffset[] tps) {
        processKafkaListenerAnnotationBeforeRegistration(endpoint, kafkaListener, bean, topics, tps);
        String containerFactory = resolve(kafkaListener.containerFactory());
        KafkaListenerContainerFactory<?> listenerContainerFactory = resolveContainerFactory(kafkaListener, containerFactory, beanName);
        //这里主要核心了,解析完成后,注册到KafkaListenerEndpointRegistry 中,等待下一步操作了
        this.registrar.registerEndpoint(endpoint, listenerContainerFactory);
        processKafkaListenerEndpointAfterRegistration(endpoint, kafkaListener);
    }

类名MethodKafkaListenerEndpoint 都可以理解成端点对象,简单地说,端点是通信通道的一端。可以理解这个端点连接业务方法和kafka 信息之间的通信端点。
@RetryableTopic 是spring kafka 2.7 后出的一个注解,主要作用就是在消费kafka信息时出现消费异常时,失败重试而出现死信信息的处理,由于Kafka内部并没有死信队列或者死信信息这类东西。Spring 自己搞出来一个DLT topics (Dead-Letter Topic),意思就是当消费信息失败到达一定次数时,会将信息发送到指定DLT topic 中。注解可以设置重试次数、重试时间、故障异常、失败策略等等。

其实这个processMainAndRetryListeners 方法跟下面processListener 作用差不多,都有解析注解内容,然后调用KafkaListenerEndpointRegistry.registerEndpoint 方法。
KafkaListenerEndpointRegistry 主要由Spring 容器创建,用于实例化MessageListenerContainer
KafkaListenerEndpointRegistrar主要代码new创建,并没有交给spring容器管理,用于帮助bean 注册到KafkaListenerEndpointRegistry中
这个两个类类名特别相似,在分析源码时被搞到晕头转向,分清楚后其实就挺简单了,这个类名搞混上浪费不算时间去理解。

注册endpoint

    public void registerEndpoint(KafkaLiEstenerEndpoint endpoint, @Nullable KafkaListenerContainerFactory<?> factory) {
        // Factory may be null, we defer the resolution right before actually creating the container
        // 这个只是一个内部类,用来装两个对象的,没有任何实现意义,factory 实际可能为空,这里使用延时创建解析这个问题
        KafkaListenerEndpointDescriptor descriptor = new KafkaListenerEndpointDescriptor(endpoint, factory);
        synchronized (this.endpointDescriptors) {
                //这个 startImmediately 并没有被初始化,这里一定是false,当被设置true,会直接创建监听器容器,这时应该是spring 容器已经初始化完成了
            if (this.startImmediately) { // Register and start immediately
                this.endpointRegistry.registerListenerContainer(descriptor.endpoint,
                        resolveContainerFactory(descriptor), true);
            }
            else {
                this.endpointDescriptors.add(descriptor);
            }
        }
    }

这里为什么有一个startImmediately开关呢,这里只是将endpoint 放入容器集中保存起来,等到全部添加完成后,使用Spring InitializingBean接口afterPropertiesSet 方法进行基础注册启动,这是利用了Spring bean 生命周期方法来触发,如果是Spring 完全启动完成后,那添加进来endpoint就是不能启动的了,所以相当于一个阈值开关,开启后立即启动。
下面看下调用KafkaListenerEndpointRegistrar.afterPropertiesSet 来开启各大endpoint 运行。

    @Override
    public void afterPropertiesSet() {
        registerAllEndpoints();
    }

    protected void registerAllEndpoints() {
        synchronized (this.endpointDescriptors) {
            for (KafkaListenerEndpointDescriptor descriptor : this.endpointDescriptors) {
                if (descriptor.endpoint instanceof MultiMethodKafkaListenerEndpoint //只有使用@KafkaHandler 才会生成这个对象
                        && this.validator != null) {
                    ((MultiMethodKafkaListenerEndpoint) descriptor.endpoint).setValidator(this.validator);
                }
                 //通过endpoint ,containerFactory 创建信息容器MessageListenerContainer 
                this.endpointRegistry.registerListenerContainer(
                        descriptor.endpoint, resolveContainerFactory(descriptor));
            }
             //全部处理完成了,就可以开启start启动按钮,让新增进来立即启动
            this.startImmediately = true;  // trigger immediate startup
        }
    }

    //获取内部类KafkaListenerContainerFactory 具体实例,在延时启动时,可能存在空,这时可以使用Spring 内部默认
   // 如果注解上已经备注了要使用ContainerFactory 则使用自定义,为空则使用默认ConcurrentKafkaListenerContainerFactory
    private KafkaListenerContainerFactory<?> resolveContainerFactory(KafkaListenerEndpointDescriptor descriptor) {
        if (descriptor.containerFactory != null) {
            return descriptor.containerFactory;
        }
        else if (this.containerFactory != null) {
            return this.containerFactory;
        }
        else if (this.containerFactoryBeanName != null) {
            Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
            this.containerFactory = this.beanFactory.getBean(
                    this.containerFactoryBeanName, KafkaListenerContainerFactory.class);
            return this.containerFactory;  // Consider changing this if live change of the factory is required
        }
        else {
        //.....
        }
    }

MessageListenerContainer

看下KafkaListenerEndpointRegistry.registerListenerContainer 方法如何生成信息监听器的。

    public void registerListenerContainer(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory) {
        registerListenerContainer(endpoint, factory, false);
    }

    public void registerListenerContainer(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory,
            boolean startImmediately) {
        String id = endpoint.getId();
        Assert.hasText(id, "Endpoint id must not be empty");
        synchronized (this.listenerContainers) {
            Assert.state(!this.listenerContainers.containsKey(id),
                    "Another endpoint is already registered with id '" + id + "'");
            //创建监听器容器
            MessageListenerContainer container = createListenerContainer(endpoint, factory);
           //使用map 将实例化容器保存起来,key就是 @KafkaListener id  ,这个就是所谓的beanName
            this.listenerContainers.put(id, container);
            ConfigurableApplicationContext appContext = this.applicationContext;
            String groupName = endpoint.getGroup();
         //如果注解中有设置自定义监听组,这时需要获取到监听组实例,将监听器容器装起来
            if (StringUtils.hasText(groupName) && appContext != null) {
                //省略部分内容
            }
            if (startImmediately) {  //如果是立即启动,这时需要手动调用监听器start 方法
                startIfNecessary(container);
            }
        }
    }

    protected MessageListenerContainer createListenerContainer(KafkaListenerEndpoint endpoint,
            KafkaListenercContainerFactory<?> factory) {
                //监听器被创建了 
        MessageListenerContainer listenerContainer = factory.createListenerContainer(endpoint);

        if (listenerContainer instanceof InitializingBean) { //这时spring 容器已经初始化完成了,生命周期方法不会再执行了,这里显式调用它
            try {
                ((InitializingBean) listenerContainer).afterPropertiesSet();
            }
            catch (Exception ex) {
                throw new BeanInitializationException("Failed to initialize message listener container", ex);
            }
        }

        int containerPhase = listenerContainer.getPhase();
        if (listenerContainer.isAutoStartup() &&
                containerPhase != AbstractMessageListenerContainer.DEFAULT_PHASE) {  // a custom phase value
            if (this.phase != AbstractMessageListenerContainer.DEFAULT_PHASE && this.phase != containerPhase) {
                throw new IllegalStateException("Encountered phase mismatch between container "
                        + "factory definitions: " + this.phase + " vs " + containerPhase);
            }
            this.phase = listenerContainer.getPhase();
        }

        return listenerContainer;
    }


    private void startIfNecessary(MessageListenerContainer listenerContainer) {
        // contextRefreshed  Spring 完全启动完成true
        if (this.contextRefreshed || listenerContainer.isAutoStartup()) {
            listenerContainer.start();
        }
    }

主要就是通过KafkaListenercContainerFactory 信息监听工厂来创建监听器MessageListenerContainer ,通过继承了SmartLifecycle。SmartLifecycle接口是Spring 在初始化完成后,根据接口isAutoStartup() 返回值是否实现该接口的类中对应的start()。Spring 当spring 完全初始化完成后,SmartLifecycle 接口就不会被Spring 调用执行,这时就需要手动执行start 方法,所以startIfNecessary 方法才会判断容器已经启动完成了。

MessageListenerContainer

    public C createListenerContainer(KafkaListenerEndpoint endpoint) {
        C instance = createContainerInstance(endpoint);
        JavaUtils.INSTANCE
                .acceptIfNotNull(endpoint.getId(), instance::setBeanName);
        if (endpoint instanceof AbstractKafkaListenerEndpoint) {
                //配置kafka 设置,因为像信息消费提交ack,信息消费批量这些设置都是通过配置设定的,这些信息都在factory保存着,这时将配置信息设置给endpoint 
            configureEndpoint((AbstractKafkaListenerEndpoint<K, V>) endpoint);
        }
        //这里是核心,将注解声明bean method 创建成MessagingMessageListenerAdapter 信息监听适配器,在将适配器初始化参数去创建信息监听器,交给instance
        endpoint.setupListenerContainer(instance, this.messageConverter);
       //将concurrency  并发数设置上
        initializeContainer(instance, endpoint);
       //自定义配置
        customizeContainer(instance);
        return instance;
    }

这时kafka 配置信息、@KafkaListener 信息、消费方法、bean 已经全部设置createListenerContainer,这时监听器容器就可以启动kafka 拉取信息,调用方法进行处理了。

直接从信息监听器ConcurrentMessageListenerContainer启动方法开始

    public final void start() {
        checkGroupId();
        synchronized (this.lifecycleMonitor) {
            if (!isRunning()) { //监听状态,测试还没有开始监听,所以监听状态应该为false
                Assert.state(this.containerProperties.getMessageListener() instanceof GenericMessageListener,
                        () -> "A " + GenericMessageListener.class.getName() + " implementation must be provided");
                 //抽象方法,由子类去实现
                doStart();
            }
        }
    }

    @Override
    protected void doStart() {
        if (!isRunning()) {
             //topic 正则匹配,根据规则去匹配sever所有topic,没有则抛出异常
            checkTopics();
            ContainerProperties containerProperties = getContainerProperties();
           //已经获取到消费组的分区和offset
            TopicPartitionOffset[] topicPartitions = containerProperties.getTopicPartitions();
            if (topicPartitions != null && this.concurrency > topicPartitions.length) {
                 // 当 concurrency  并发数超过分区时,这里会打印警告日志
                this.logger.warn(() -> "When specific partitions are provided, the concurrency must be less than or "
                        + "equal to the number of partitions; reduced from " + this.concurrency + " to "
                        + topicPartitions.length);
                 //注意这里,强制将并发数改成最大分数,在设置消费并发时,不用担心分区数量并发超过
                this.concurrency = topicPartitions.length;
            }
            setRunning(true); //开始监听
                //concurrency 就是创建容器时,从@KafkaListener 解析处理的并发数
              // 可以看出并发数控制着  KafkaMessageListenerContainer 实例产生
            for (int i = 0; i < this.concurrency; i++) {
                //创建 KafkaMessageListenerContainer 对象
                KafkaMessageListenerContainer<K, V> container =
                        constructContainer(containerProperties, topicPartitions, i);
               //配置监听器容器拦截器、通知这些,如果没有配置默认都是null
                configureChildContainer(i, container);
                if (isPaused()) {
                    container.pause();
                }
                container.start(); //启动任务
                //因为所有消费现场都是同一个容器创建的,当要停止某个消费topic,需要对containers进行操作
                this.containers.add(container);
            }
        }
    }

    private KafkaMessageListenerContainer<K, V> constructContainer(ContainerProperties containerProperties,
            @Nullable TopicPartitionOffset[] topicPartitions, int i) {

        KafkaMessageListenerContainer<K, V> container;
        if (topicPartitions == null) {
            container = new KafkaMessageListenerContainer<>(this, this.consumerFactory, containerProperties); // NOSONAR
        }
        else { //如果存在分区,每一个消费都有平分分区
            container = new KafkaMessageListenerContainer<>(this, this.consumerFactory, // NOSONAR
                    containerProperties, partitionSubset(containerProperties, i));
        }
        return container;
    }

看到了@KafkaListener 并发数如何实现的,并且并发数不能超过分区数的,如果并发数小于分区数,则会出现平分的情况,可能会让一个消费占有多个分区情况。这里在创建KafkaMessageListenerContainer 去对Kafka topic 进行消费。

KafkaMessageListenerContainer

因为KafkaMessageListenerContainer和ConcurrentMessageListenerContainer都是通过extends AbstractMessageListenerContainer 重写doStart()开启任务,直接看见doStart就可以知道程序入口了。

    protected void doStart() {
        if (isRunning()) {
            return;
        }
        if (this.clientIdSuffix == null) { // stand-alone container
            checkTopics();
        }
        ContainerProperties containerProperties = getContainerProperties();
        //检查是否非自动ack,在org.springframework.kafka.listener.ContainerProperties.AckMode 有多种模式
        checkAckMode(containerProperties);
        // 
        Object   = containerProperties.getMessageListener();
         //任务执行器,看起俩像一个线程池Executor ,本质上是直接使用Thread来启动任务的
        AsyncListenableTaskExecutor consumerExecutor = containerProperties.getConsumerTaskExecutor();
        if (consumerExecutor == null) {
            consumerExecutor = new SimpleAsyncTaskExecutor(
                    (getBeanName() == null ? "" : getBeanName()) + "-C-");
            containerProperties.setConsumerTaskExecutor(consumerExecutor);
        }
        GenericMessageListener<?> listener = (GenericMessageListener<?>) messageListener;
         //这个一个枚举类,根据类型生成type,type 标记着如何处理kafka 信息,有批量的、单条的、手动提交、自动提交
        ListenerType listenerType = determineListenerType(listener);
           //ListenerConsumer 内部类,有关Kafka 任何信息都可以直接去取的
        this.listenerConsumer = new ListenerConsumer(listener, listenerType);
        setRunning(true); //设置运行状态
        this.startLatch = new CountDownLatch(1);
        this.listenerConsumerFuture = consumerExecutor
                .submitListenable(this.listenerConsumer);//启动线程
        try {
            if (!this.startLatch.await(containerProperties.getConsumerStartTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
                this.logger.error("Consumer thread failed to start - does the configured task executor "
                        + "have enough threads to support all containers and concurrency?");
                publishConsumerFailedToStart();
            }
        }
        catch (@SuppressWarnings(UNUSED) InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

在这里主要逻辑就是启动线程去去处理kafka 信息拉取。我们直接去看ListenerConsumer run() 就行了。

        @Override // NOSONAR complexity
        public void run() {
            ListenerUtils.setLogOnlyMetadata(this.containerProperties.isOnlyLogRecordMetadata());
               //向spring容器发布事件
            publishConsumerStartingEvent();
            this.consumerThread = Thread.currentThread();
            setupSeeks();
            KafkaUtils.setConsumerGroupId(this.consumerGroupId);
            this.count = 0;
            this.last = System.currentTimeMillis();
               //从kafka 获取消费组 分区 offset,保存起来
            initAssignedPartitions();
             //发布事件
            publishConsumerStartedEvent();
            Throwable exitThrowable = null;
            while (isRunning()) {
                try {
                            //核心  拉取信息和 调用方法去处理信息
                    pollAndInvoke();
                }
                //省略

pollAndInvoke 这个方法就是拉取信息和处理的过程了,方法太繁琐了,无非就是如何去调用endpoint 生成信息处理器,并且将参数注入方法中。

总结

image.png

结合上面图,简单总结下Spring Kafka 如何通过一个简单注解实现对方法消费信息的。首先通过Spring 前置处理器机制使用KafkaListenerAnnotationBeanPostProcessor 扫描所有已经实例化的bean,找出带有@KafkaListener bean 和方法,解析注解的内容设置到MethodKafkaListenerEndpoint,并且注册到KafkaListenerEndpointRegistry,有它统一保存起来,等到执行前置处理器统一将KafkaListenerEndpointRegistry保存起来的enpoint,注册到KafkaListenerEndpointRegistrar,根据enpoint生成ConcurrentMessageListenerContainer,在根据并发数去生成对应数量的KafkaMessageListenerContainer,最后使用Thread 异步启动Kafka 信息拉去,调用bean 方法进行处理。
还理解了topic 分区和并发数如何关联的,还知道kafka消费是可控制的,处理Kafka信息方法,返回值可以被推送到另一个topic的、也是第一次知道有@RetryableTopic 重试机制,还有DLT 死信topic。如果不是看源码分析,平常工作场景估计很少用得上这些。现在看源码多了,越来越有感觉查看代码更能加深你对框架学习,心得。

动态订阅

看了这么多代码,对照处理器CV下就,简单版动态监听就可以实现了

@Component
public class ListenerMessageCommand<K,V> implements CommandLineRunner {

    @Autowired
    private Cusmotd cusmotd;

    @Autowired
    private KafkaListenerEndpointRegistry endpointRegistry;

    @Autowired
    private KafkaListenerContainerFactory<?> kafkaListenerContainerFactory;

    private Logger logger = LoggerFactory.getLogger(ListenerMessageCommand.class);

    @Override
    public void run(String... args) throws Exception {
        MethodKafkaListenerEndpoint<K, V> endpoint = new MethodKafkaListenerEndpoint<>();
        endpoint.setBean(cusmotd);
        Method method = ReflectionUtils.findMethod(cusmotd.getClass(), "dis", ConsumerRecord.class);
        endpoint.setMethod(method);
        endpoint.setMessageHandlerMethodFactory(new DefaultMessageHandlerMethodFactory());
        endpoint.setId("tk.shengyifeng.custom#1");
        endpoint.setGroupId("test");
        endpoint.setTopicPartitions(new TopicPartitionOffset[0]);
        endpoint.setTopics("skdsk");
        endpoint.setClientIdPrefix("comuserd_");
        endpoint.setConcurrency(1);
        endpointRegistry.registerListenerContainer(endpoint,kafkaListenerContainerFactory,true);
        logger.info("register...............");
    }
}

我们看过完整代码,知道监听动作是由KafkaListenerContainerFactory创建后,调用实例start 方法开始的,并且我们还能拿到监听容器对象,可以调用对象各式API,可以动态停止对topic消费哦。

@RestController
@RequestMapping("kafka")
public class KafkaController<K,V> {
    @Autowired
    private Cusmotd cusmotd;

    @Autowired
    private KafkaListenerContainerFactory<?> kafkaListenerContainerFactory;

    private Map<String,MessageListenerContainer> containerMap = new ConcurrentReferenceHashMap<>();

    @GetMapping("start/topic")
    public void startTopic(String topicName,String groupName){
        MethodKafkaListenerEndpoint<K, V> endpoint = new MethodKafkaListenerEndpoint<>();
        endpoint.setBean(cusmotd);
        Method method = ReflectionUtils.findMethod(cusmotd.getClass(), "dis", ConsumerRecord.class);
        endpoint.setMethod(method);
        endpoint.setMessageHandlerMethodFactory(new DefaultMessageHandlerMethodFactory());
        endpoint.setId("tk.shengyifeng.custom#1");
        endpoint.setGroupId(groupName);
        endpoint.setTopicPartitions(new TopicPartitionOffset[0]);
        endpoint.setTopics(topicName);
        endpoint.setClientIdPrefix("comuserd_");
        endpoint.setConcurrency(1);
        MessageListenerContainer listenerContainer = kafkaListenerContainerFactory.createListenerContainer(endpoint);
        listenerContainer.start();
        containerMap.put(topicName,listenerContainer);
    }

    @GetMapping("stop/topic")
    public void stopTopic(String topicName){
        if (containerMap.containsKey(topicName))
            containerMap.get(topicName).stop();
    }
}

这个简单http接口,通过接口方式支持对外扩容的方式动态订阅频道,并且支持已经订阅topic消费停下来。
使用@kafkaListener 声明方法消费的同学不用羡慕的,Spring 提供机制可以去获取MessageListenerContainer,上面代码分析我们知道了KafkaListenerEndpointRegistry内部的listenerContainers 会保存所有container实例,并且提供外部方法根据id去获取对象,而且KafkaListenerEndpointRegistry还是有spring 进行实例化的,所以....
为了方便获取id简单,可以在使用注解时,手动指定id 值,如果没有指定则id,默认生成规则是org.springframework.kafka.KafkaListenerEndpointContainer# + 自增长

SpringBoot 自动配置

大家可能好奇,Spring boot中Kafka配置信息如何给kafkaListenerContainerFactory,因为它是通过Spring 容器初始化的,源码中并没有看见带有构造器的参数注入。想要具体了解,只有看KafkaAnnotationDrivenConfiguration,ConcurrentKafkaListenerContainerFactoryConfigurer

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableKafka.class)
class KafkaAnnotationDrivenConfiguration {

    private final KafkaProperties properties;

    private final RecordMessageConverter messageConverter;

    private final RecordFilterStrategy<Object, Object> recordFilterStrategy;

    private final BatchMessageConverter batchMessageConverter;

    private final KafkaTemplate<Object, Object> kafkaTemplate;

    private final KafkaAwareTransactionManager<Object, Object> transactionManager;

    private final ConsumerAwareRebalanceListener rebalanceListener;

    private final ErrorHandler errorHandler;

    private final BatchErrorHandler batchErrorHandler;

    private final AfterRollbackProcessor<Object, Object> afterRollbackProcessor;

    private final RecordInterceptor<Object, Object> recordInterceptor;

    KafkaAnnotationDrivenConfiguration(KafkaProperties properties,
            ObjectProvider<RecordMessageConverter> messageConverter,
            ObjectProvider<RecordFilterStrategy<Object, Object>> recordFilterStrategy,
            ObjectProvider<BatchMessageConverter> batchMessageConverter,
            ObjectProvider<KafkaTemplate<Object, Object>> kafkaTemplate,
            ObjectProvider<KafkaAwareTransactionManager<Object, Object>> kafkaTransactionManager,
            ObjectProvider<ConsumerAwareRebalanceListener> rebalanceListener, ObjectProvider<ErrorHandler> errorHandler,
            ObjectProvider<BatchErrorHandler> batchErrorHandler,
            ObjectProvider<AfterRollbackProcessor<Object, Object>> afterRollbackProcessor,
            ObjectProvider<RecordInterceptor<Object, Object>> recordInterceptor) {
        this.properties = properties;
        this.messageConverter = messageConverter.getIfUnique();
        this.recordFilterStrategy = recordFilterStrategy.getIfUnique();
        this.batchMessageConverter = batchMessageConverter
                .getIfUnique(() -> new BatchMessagingMessageConverter(this.messageConverter));
        this.kafkaTemplate = kafkaTemplate.getIfUnique();
        this.transactionManager = kafkaTransactionManager.getIfUnique();
        this.rebalanceListener = rebalanceListener.getIfUnique();
        this.errorHandler = errorHandler.getIfUnique();
        this.batchErrorHandler = batchErrorHandler.getIfUnique();
        this.afterRollbackProcessor = afterRollbackProcessor.getIfUnique();
        this.recordInterceptor = recordInterceptor.getIfUnique();
    }

作为其实Spring Boot自动配置原理就是由spring-boot-autoconfigure 包编码实现的,在根据@ConditionalOnClass 注解来决定是否启动配置类,所以当你引入对应pox时,就会启动配置类了,配置信息会注入到KafkaProperties对象中,然后将properties 设置到工厂对象,实例化对象交给spring 容器,你会发现大多数自动配置都是这样套路。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容