前述
在kafka提供的原生java客户端中,消费者采用while(true){…}的方式进行消息拉取。这一点理解起来还是比较容易的。而用SpringBoot集成Kafka后,我们使用了SpringBoot提供的@KafkaListener注解,去监听消息。这让我不禁产生疑惑:消息是怎么监听过来的呢?怎么实现监听的呢?带着疑问,花费了我一天的时间,去探索其中的原理。由于水平有限,只是探索到了重要的几个步骤,没有探索出来其来龙去脉。下面记录一下这一整天的探索流程。
一、探索过程
首先,对于Spring项目而言,一切的一切,都起源于配置,所以,先看有关Kafka消费者在SpringBoot中的配置,如下:
@Configuration
@EnableKafka
public class KafkaConsumerConfig {
@Value("${kafka.consumer.servers}")
private String servers;
@Value("${kafka.consumer.enable.auto.commit}")
private boolean enableAutoCommit;
@Value("${kafka.consumer.session.timeout}")
private String sessionTimeout;
@Value("${kafka.consumer.auto.commit.interval}")
private String autoCommitInterval;
@Value("${kafka.consumer.group.id}")
private String groupId;
@Value("${kafka.consumer.auto.offset.reset}")
private String autoOffsetReset;
@Value("${kafka.consumer.concurrency}")
private int concurrency;
@Autowired
ConsumerRebalanceListener consumerRebalanceListener;
public Map<String, Object> consumerConfigsAck() {
Map<String, Object> propsMap = new HashMap<>();
propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return propsMap;
}
public ConsumerFactory<String, String> consumerFactoryAck() {
return new DefaultKafkaConsumerFactory<>(consumerConfigsAck());
}
@Bean("listenerAck")
public ConsumerListenerAck listenerAck() {
return new ConsumerListenerAck();
}
@Bean("factoryAck")
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>>
kafkaListenerContainerFactoryAck() {
ConcurrentKafkaListenerContainerFactory<String, String> factory
= new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactoryAck());
factory.setConcurrency(concurrency);
factory.getContainerProperties().setPollTimeout(1500);
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
factory.getContainerProperties().setConsumerRebalanceListener(consumerRebalanceListener);
return factory;
}
}
- 猜想1.观察者模式实现
进行猜想和分析。首先,我们猜想,@KafkaListener是一个监听器,监听到Consumer获取到对象,然后获取这些信息,采用的是观察者模式(观察者模式)。由观察者模式的实现思路我们可以知道,Consumer中,要维护一个监听器列表,当Consumer监听到消息时,调用监听器列表中监听器的监听方法,讲消息传递给监听器。按照这个猜想,我们来看Consumer对象。
在上面的配置中,配置了ConsumerFactory消费者工厂,顾名思义,消费者工厂肯定是生产消费者对象的。我们可以看到ConsumerFactory工厂中定义了创建消费者对象的方法,如下:
public interface ConsumerFactory<K, V> {
/**
* Create a consumer with the group id and client id as configured in the properties.
* @return the consumer.
*/
default Consumer<K, V> createConsumer() {
return createConsumer(null);
}
/**
* Create a consumer, appending the suffix to the {@code client.id} property,
* if present.
* @param clientIdSuffix the suffix.
* @return the consumer.
* @since 1.3
*/
default Consumer<K, V> createConsumer(@Nullable String clientIdSuffix) {
return createConsumer(null, clientIdSuffix);
}
/**
* Create a consumer with an explicit group id; in addition, the
* client id suffix is appended to the {@code client.id} property, if both
* are present.
* @param groupId the group id.
* @param clientIdSuffix the suffix.
* @return the consumer.
* @since 1.3
*/
default Consumer<K, V> createConsumer(@Nullable String groupId, @Nullable String clientIdSuffix) {
return createConsumer(groupId, null, clientIdSuffix);
}
/**
* Create a consumer with an explicit group id; in addition, the
* client id suffix is appended to the clientIdPrefix which overrides the
* {@code client.id} property, if present.
* @param groupId the group id.
* @param clientIdPrefix the prefix.
* @param clientIdSuffix the suffix.
* @return the consumer.
* @since 2.1.1
*/
Consumer<K, V> createConsumer(@Nullable String groupId, @Nullable String clientIdPrefix,
@Nullable String clientIdSuffix);
/**
* Create a consumer with an explicit group id; in addition, the
* client id suffix is appended to the clientIdPrefix which overrides the
* {@code client.id} property, if present. In addition, consumer properties can
* be overridden if the factory implementation supports it.
* @param groupId the group id.
* @param clientIdPrefix the prefix.
* @param clientIdSuffix the suffix.
* @param properties the properties to override.
* @return the consumer.
* @since 2.2.4
*/
default Consumer<K, V> createConsumer(@Nullable String groupId, @Nullable String clientIdPrefix,
@Nullable String clientIdSuffix, @Nullable Properties properties) {
return createConsumer(groupId, clientIdPrefix, clientIdSuffix);
}
/**
* Return true if consumers created by this factory use auto commit.
* @return true if auto commit.
*/
boolean isAutoCommit();
/**
* Return an unmodifiable reference to the configuration map for this factory.
* Useful for cloning to make a similar factory.
* @return the configs.
* @since 2.0
*/
default Map<String, Object> getConfigurationProperties() {
throw new UnsupportedOperationException("'getConfigurationProperties()' is not supported");
}
/**
* Return the configured key deserializer (if provided as an object instead
* of a class name in the properties).
* @return the deserializer.
* @since 2.0
*/
@Nullable
default Deserializer<K> getKeyDeserializer() {
return null;
}
/**
* Return the configured value deserializer (if provided as an object instead
* of a class name in the properties).
* @return the deserializer.
* @since 2.0
*/
@Nullable
default Deserializer<V> getValueDeserializer() {
return null;
}
}
那么我们看Consumer,它也是一个接口,是kafka定义的接口,我们找其实现类,如下:
两个实现类,用的肯定是KafkaConsumer,所以我们看KafkaConsumer的源码,查找里面的Listener列表。KafkaConsumer是一个庞大的类,定义了很多东西,我们不可能全部看完,只找重点,直接搜"listener"关键字,在导包的地方,我们可以搜出来两个listeners的字眼:
这说明,KafkaConsumer类里用到了这两个listeners相关的类,我们一个一个看。
首先看NoOpConsumerRebalanceListener源码,它实现了ConsumerRebalanceListener接口,是一个分区再均衡监听器,这明显与消息接收的监听器不一样,所以,这个listener不是我们要找的原理。
public class NoOpConsumerRebalanceListener implements ConsumerRebalanceListener {
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {}
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {}
}
然后,再看ClusterResourceListeners,看其字面意思,是一个Listener的集合,这不正好就是我们按照观察者模式思路要找的监听器集合吗?心里激动的一批,点开其源码进行查看:
public class ClusterResourceListeners {
private final List<ClusterResourceListener> clusterResourceListeners;
public ClusterResourceListeners() {
this.clusterResourceListeners = new ArrayList<>();
}
/**
* Add only if the candidate implements {@link ClusterResourceListener}.
* @param candidate Object which might implement {@link ClusterResourceListener}
*/
public void maybeAdd(Object candidate) {
if (candidate instanceof ClusterResourceListener) {
clusterResourceListeners.add((ClusterResourceListener) candidate);
}
}
/**
* Add all items who implement {@link ClusterResourceListener} from the list.
* @param candidateList List of objects which might implement {@link ClusterResourceListener}
*/
public void maybeAddAll(List<?> candidateList) {
for (Object candidate : candidateList) {
this.maybeAdd(candidate);
}
}
/**
* Send the updated cluster metadata to all {@link ClusterResourceListener}.
* @param cluster Cluster metadata
*/
public void onUpdate(ClusterResource cluster) {
for (ClusterResourceListener clusterResourceListener : clusterResourceListeners) {
clusterResourceListener.onUpdate(cluster);
}
}
}
可以看到,ClusterResourceListeners内部维护了一个ClusterResourceListener列表,观察者模式的思路越来越清晰,我们看ClusterResourceListener的源码:
public interface ClusterResourceListener {
/**
* A callback method that a user can implement to get updates for {@link ClusterResource}.
* @param clusterResource cluster metadata
*/
void onUpdate(ClusterResource clusterResource);
}
里面的onUpdate方法实锤了,这就是观察者模式。我们的思路似乎得到了证实。那它是怎么实现消息发到ClusterResourceListener 里的呢,具体实现是什么呢?我们要找ClusterResourceListener 的实现类去看。搜索ClusterResourceListener 的实现类发现,尼玛,居然没有实现类:
再看ClusterResourceListener源码的注释,第一句就说明了情况:
由注释可知,此接口是为用户留出来的接口,可以利用该接口在接收数据时做一些事情。而@KafkaListener监听的消息,并不是用的此接口。
至此,观察者模式思想实现@KafkaListener的猜想失败了。KafkaConsumer里是利用了观察者模式,但并不是利用在了@KafkaListener上面。继续猜想。
- 猜想2:SpringBoot与Kafka消费者的配置中入手
从KafkaConsumer类入手失败了,那只能转换思路了,既然不是KafkaConsumer接收到消息后主动往监听器里推,那就是其他地方调用了KafkaConsumer的poll()方法,拉取来数据,然后再往监听器里塞。
上面说到,一切的一切,都从配置开始,我们分析Kafka消费者在SpringBoot中的配置,请查看上面的配置代码。分析可以得知,最终配置的属性也好,对象也好,就集中到了KafkaListenerContainerFactory里,如下图:
所以,我们猜想,这个类,是Kafka工作的一个核心类。我们又知道,spring中的配置,是项目在初始化的时候用来加载用的,所以,在项目启动过程中,只是初始化一些工厂实例,并填充实例的一些属性。而真正的接收消息,塞入监听,这些逻辑,肯定不在初始化的逻辑中。所以,虽然我们找到了核心类,但是还需要猜测核心类中的核心流程。
我们看KafkaListenerContainerFactory的源码:
/**
* Factory for {@link MessageListenerContainer}s.
*
* @param <C> the {@link MessageListenerContainer} implementation type.
*
* @author Stephane Nicoll
* @author Gary Russell
*
* @see KafkaListenerEndpoint
*/
public interface KafkaListenerContainerFactory<C extends MessageListenerContainer> {
/**
* Create a {@link MessageListenerContainer} for the given {@link KafkaListenerEndpoint}.
* Containers created using this method are added to the listener endpoint registry.
* @param endpoint the endpoint to configure
* @return the created container
*/
C createListenerContainer(KafkaListenerEndpoint endpoint);
/**
* Create and configure a container without a listener; used to create containers that
* are not used for KafkaListener annotations. Containers created using this method
* are not added to the listener endpoint registry.
* @param topicPartitions the topicPartitions to assign.
* @deprecated in favor of {@link #createContainer(TopicPartitionOffset[])}.
* @return the container.
* @since 2.2
*/
@Deprecated
C createContainer(Collection<org.springframework.kafka.support.TopicPartitionInitialOffset> topicPartitions);
/**
* Create and configure a container without a listener; used to create containers that
* are not used for KafkaListener annotations. Containers created using this method
* are not added to the listener endpoint registry.
* @param topicPartitions the topicPartitions to assign.
* @return the container.
* @since 2.3
*/
C createContainer(TopicPartitionOffset... topicPartitions);
/**
* Create and configure a container without a listener; used to create containers that
* are not used for KafkaListener annotations. Containers created using this method
* are not added to the listener endpoint registry.
* @param topics the topics.
* @return the container.
* @since 2.2
*/
C createContainer(String... topics);
/**
* Create and configure a container without a listener; used to create containers that
* are not used for KafkaListener annotations. Containers created using this method
* are not added to the listener endpoint registry.
* @param topicPattern the topicPattern.
* @return the container.
* @since 2.2
*/
C createContainer(Pattern topicPattern);
}
可以看到,该工厂是生产MessageListenerContainer对象的工厂,所以,我们猜测,核心流程,就在MessageListenerContainer对象里定义的。
MessageListenerContainer是一个接口,我们看其源码注释:
由注释可以看到,其是内部使用的一个监听器接口,这符合我们预期的逻辑,因为消息的监听是框架内部自身实现的,我们没有做任何操作,只是定义了@KafkaListener下面的方法。
我们看MessageListenerContainer的实现类都有哪些:
可以看到,下面有一个抽象类,抽象类下又有两个实现类。我们看ConcurrentMessageListenerContainer类的源码:(因为我们采用的是多消费者并发模式,这个类字眼明显是并发消息的监听器,所以看这个类)。
通过上图的源码可知,ConcurrentMessageListenerContainer中又定义了另一个实现类KafkaMessageListenerContainer的集合。所以这两个类其实是有交互的。那么我们如何找这个类中我们想要的方法呢?上面我们提到,现在我们的猜想是这个类去调用KafkaConsumer的poll方法,获得消息后再传入@KafkaListener定义的监听器中,所以,我们在ConcurrentMessageListenerContainer中搜索“consumer”字样,看都在哪里应用了。通过搜索可知,ConcurrentMessageListenerContainer类中,有consumerFactory的使用,下面截图是使用处:
ConcurrentMessageListenerContainer类只有这两个地方应用到了cosumerFactory。所以,我们要看KafkaMessageListenerContainer类中,如何应用的consumerFactory。
打开KafkaMessageListenerContainer的源码,搜索关键字"consumer", 可以发现,里面的“consumer”字眼很多,排查起来很困难。那我们再分析,核心流程肯定是调用consumer的poll方法,获得消息,才能给监听器。所以,我们搜索"poll("关键字,搜索这个关键字,我们找到了关键的流程所在:
下面我们查看invokeListener方法,求证是否是我们要找的逻辑,源码如下:
private void invokeListener(final ConsumerRecords<K, V> records) {
if (this.isBatchListener) {
invokeBatchListener(records);
}
else {
invokeRecordListener(records);
}
}
这里,我们选择单条的处理invokeRecordListener方法(其实选哪个都行,一个是单条,一个是批量,本质逻辑是一样的):
private void invokeRecordListener(final ConsumerRecords<K, V> records) {
if (this.transactionTemplate != null) {
invokeRecordListenerInTx(records);
}
else {
doInvokeWithRecords(records);
}
}
然后,选doInvokeWithRecords方法,查看源码:
private void doInvokeWithRecords(final ConsumerRecords<K, V> records) {
Iterator<ConsumerRecord<K, V>> iterator = records.iterator();
while (iterator.hasNext()) {
final ConsumerRecord<K, V> record = iterator.next();
this.logger.trace(() -> "Processing " + record);
doInvokeRecordListener(record, null, iterator);
if (this.nackSleep >= 0) {
handleNack(records, record);
break;
}
}
}
这里,再找到关键步骤doInvokeRecordListener方法,查看其源码,(不再一一罗列),然后,在进入下一个关键步骤invokeOnMessage,查看源码,然后,再进入关键步骤doInvokeOnMessage查看源码,这里,进入了关键步骤,我们分析一下其源码:
private void doInvokeOnMessage(final ConsumerRecord<K, V> recordArg) {
ConsumerRecord<K, V> record = recordArg;
if (this.recordInterceptor != null) {
record = this.recordInterceptor.intercept(record);
}
if (record == null) {
this.logger.debug(() -> "RecordInterceptor returned null, skipping: " + recordArg);
}
else {
switch (this.listenerType) {
case ACKNOWLEDGING_CONSUMER_AWARE:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null, this.consumer);
break;
case CONSUMER_AWARE:
this.listener.onMessage(record, this.consumer);
break;
case ACKNOWLEDGING:
this.listener.onMessage(record,
this.isAnyManualAck
? new ConsumerAcknowledgment(record)
: null);
break;
case SIMPLE:
this.listener.onMessage(record);
break;
}
}
}
上面我们可以看到,不同类型的listener,调用了不同类型listener的onmessage方法。而onmessage方法,是GenericMessageListener接口定义的。我们查看GenericMessageListener的实现类,去找onmessage的实现方法,
可以看到,有两个大的实现类,一个是Batch的,一个是单条的,这正好和上面的单条处理和批量处理相对应,我们看单条处理的类:
最终选定RecordMessagingMessageListenerAdapter作为分析对象。因为从字面意思上看这个类最合适。我们看其onmessage方法的注释:
从注释来看,这里就是将消息传给监听器的入口。我们看其源码:
public void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
Message<?> message;
if (isConversionNeeded()) {
message = toMessagingMessage(record, acknowledgment, consumer);
}
else {
message = NULL_MESSAGE;
}
if (logger.isDebugEnabled()) {
logger.debug("Processing [" + message + "]");
}
try {
Object result = invokeHandler(record, acknowledgment, message, consumer);
if (result != null) {
handleResult(result, record, message);
}
}
catch (ListenerExecutionFailedException e) { // NOSONAR ex flow control
if (this.errorHandler != null) {
try {
if (message.equals(NULL_MESSAGE)) {
message = new GenericMessage<>(record);
}
Object result = this.errorHandler.handleError(message, e, consumer);
if (result != null) {
handleResult(result, record, message);
}
}
catch (Exception ex) {
throw new ListenerExecutionFailedException(createMessagingErrorMessage(// NOSONAR stack trace loss
"Listener error handler threw an exception for the incoming message",
message.getPayload()), ex);
}
}
else {
throw e;
}
}
}
关键步骤是
点进去看其源码:
protected final Object invokeHandler(Object data, Acknowledgment acknowledgment, Message<?> message,
Consumer<?, ?> consumer) {
try {
if (data instanceof List && !this.isConsumerRecordList) {
return this.handlerMethod.invoke(message, acknowledgment, consumer);
}
else {
return this.handlerMethod.invoke(message, data, acknowledgment, consumer);
}
}
catch (org.springframework.messaging.converter.MessageConversionException ex) {
if (this.hasAckParameter && acknowledgment == null) {
throw new ListenerExecutionFailedException("invokeHandler Failed",
new IllegalStateException("No Acknowledgment available as an argument, "
+ "the listener container must have a MANUAL AckMode to populate the Acknowledgment.",
ex));
}
throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " +
"be invoked with the incoming message", message.getPayload()),
new MessageConversionException("Cannot handle message", ex));
}
catch (MessagingException ex) {
throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " +
"be invoked with the incoming message", message.getPayload()), ex);
}
catch (Exception ex) {
throw new ListenerExecutionFailedException("Listener method '" +
this.handlerMethod.getMethodAsString(message.getPayload()) + "' threw exception", ex);
}
}
可以看到,这里的逻辑很复杂,具体使用哪个逻辑,我们在下面的验证环节,通过debug来查看。
由此,可以判定,@KafkaListener定义的监听,就是由这里进行消息传入的。
二、验证
上面我们猜想2基本证实了@KafkaListener的流程,但是最后一步的实现逻辑,我们没有进行分析,下面我们就采用debug模式,来验证我们的猜想,并屡清楚最后的逻辑。
首先,我们先屡一下思路,看在哪里打断点。从上面的分析逻辑中可以看到,我们定位到核心流程,就是查询poll方法,定位到的,所以,断点应该从查询到的poll方法中开始,我们可以看到,调用poll方法的父级方法是pollAndInvoke方法,我们再往上追一级,调用pollAndInvoke方法的父级方法是run方法,源码如下:
我们从这里开始打断点,以便以理解其来龙去脉。
首先我们由断点可以发现,当没有消息时,断点也会不断的进入pollAndInvoke方法,因为这里用了while (isRunning()),启动状态,就一直循环。这里也类似于循环拉取的意思。
然后,我们直接进入最关键的onmessage方法的debug:
这里进入了第二个逻辑判断,省略中间步骤,进入终极步骤:
就是在这里,调用了方法,执行了监听器方法。
至此,其工作流程就算找到了。
三、总结
上面只是找到了其工作流程,下面我们进行一下总结:
1.首先是KafkaMessageListenerContainer类调用Consumer的poll方法,获得record对象。
2.KafkaMessageListenerContainer类,顾名思义,是装MessageListener的容器,所以它里面必定装了MessageListener。根据代码可知,其装的是GenericMessageListener接口类型的listener。
3.GenericMessageListener类里,在初始化的时候,装载了@KafkaListener修饰的类和方法
4.KafkaMessageListenerContainer类调用GenericMessageListener的onmessage方法,将record对象传入参数,GenericMessageListener获取@KafkaListener修饰的方法,并将record对象传入,最终通过反射,执行@KafkaListener修饰的方法。
总体来说,就是GenericMessageListener装载@KafkaListener类和方法,KafkaMessageListenerContainer是个中介,将Consumer和GenericMessageListener联系起来,达到了consumer获取消息,GenericMessageListener执行监听方法的效果。
总之就是先调用consumer的poll方法,得到record数据,然后利用反射,将@KafkaListener中的方法通过反射执行,并将record传入参数。
四、不足
由于精力和水平有限,本文并没有说明在哪里解析的@KafkaListener注解,并将该注解的修饰内容装载到GenericMessageListener类中的。也没有说明KafkaMessageListenerContainer对象中的GenericMessageListener类是如何加进去的。还望有大神能够指引一二。
五、感悟
由上面的源码分析可以看到,我们在阅读源码时,首选要有大胆猜测的勇气,如果是自己,怎么实现这个功能。然后按照这个思路,去找源码中的关键类,关键代码。可以采用关键字搜索的功能,去排查哪里是关键点。最后,将源码的整个流程串联起来,理解其设计和实现。源码的阅读本身就是了解其实现原理和设计理念,我们无需对源码的每个步骤都进行解读。
六、最后
费了九牛二虎之力,把源码解读出来,最后,我们画出其设计理念图,有利于我们在工作中进行应用。否则,我们分析半天的源码,过段时间就随风而逝了。
所以,实现监听器的另一种思路就是创建一个中介对象,由中介对象去维护观察者和被观察者,调用观察者改变的方式,同时通知被观察者进行改变。