@EnableKafka注解
此注解会import一个类KafkaBootstrapConfiguration,这个类会创建2个bean:KafkaListenerAnnotationBeanPostProcessor和KafkaListenerEndpointRegistry
@Configuration
public class KafkaBootstrapConfiguration {
@SuppressWarnings("rawtypes")
@Bean(name = KafkaListenerConfigUtils.KAFKA_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public KafkaListenerAnnotationBeanPostProcessor kafkaListenerAnnotationProcessor() {
return new KafkaListenerAnnotationBeanPostProcessor();
}
@Bean(name = KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
public KafkaListenerEndpointRegistry defaultKafkaListenerEndpointRegistry() {
return new KafkaListenerEndpointRegistry();
}
}
KafkaListenerAnnotationBeanPostProcessor方法详解(扫描包含KafkaListener和KafkaListeners注解的bean和method并注册endpoint)
public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
//扫描所有类上是否有注解,结合KafkaHandler注解一起使用
Collection<KafkaListener> classLevelListeners = findListenerAnnotations(targetClass);
final boolean hasClassLevelListeners = classLevelListeners.size() > 0;
final List<Method> multiMethods = new ArrayList<>();
//扫描当前bean的所有method是否有KafkaListener注解,并且将方法上有此注解放入在map中
Map<Method, Set<KafkaListener>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<KafkaListener>>) method -> {
Set<KafkaListener> listenerMethods = findListenerAnnotations(method);
return (!listenerMethods.isEmpty() ? listenerMethods : null);
});
//如果类上面有KafkaListener注解,继续扫面当前类的所有方法中是否有@KafkaHandler注解,个人理解这两个注解结合使用的场景是用在
//可以减少同一个topic消费时候的对象参数转换,最终将扫描的结果放在Set对象中
if (hasClassLevelListeners) {
Set<Method> methodsWithHandler = MethodIntrospector.selectMethods(targetClass,
(ReflectionUtils.MethodFilter) method ->
AnnotationUtils.findAnnotation(method, KafkaHandler.class) != null);
multiMethods.addAll(methodsWithHandler);
}
//nonAnnotatedClasses这个属性后面阅读源码的过程中没看到使用,先不管
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(bean.getClass());
if (this.logger.isTraceEnabled()) {
this.logger.trace("No @KafkaListener annotations found on bean type: " + bean.getClass());
}
}else {
//如果当前项目扫描到了@KafkaListener注解标注的方法,则去循环去注册endpoint(这里的一个方法的一个注解就是一个endpoint,因为一个方法可以监听不同的队列的)
for (Map.Entry<Method, Set<KafkaListener>> entry : annotatedMethods.entrySet()) {
Method method = entry.getKey();
for (KafkaListener listener : entry.getValue()) {
//主要看这里,处理方法上包含KafkaListener的method
processKafkaListener(listener, method, bean, beanName);
}
}
}
if (hasClassLevelListeners) {
//主要看这里,处理类上面包含KafkaListener和方法上包含KafkaHandler的注册,和上面processKafkaListener最终调用的是一个方法processListener,只不过这个是一个注解对应
//多个方法,上面是一个注解对应一个方法,所以这里不再展开,直接看processListener方法。都是去注册endpoint,注意一个KafkaListener注解和endpoint是一一对应的。不管标记在
//方法上还是类上
processMultiMethodListeners(classLevelListeners, multiMethods, bean, beanName);
}
}
return bean;
}
最终都会调用到processListener方法中(从注解中获取数据放在endpoint对象中并注册KafkaListenerEndpointRegistrar,每一个KafkaListener标注的方法或者类为一个endpoint)
protected void processListener(MethodKafkaListenerEndpoint<?, ?> endpoint, KafkaListener kafkaListener,
Object bean, Object adminTarget, String beanName) {
//beanRef是为当前的ConsumerListener对象设置一个别名,key为你设置的别名,value为当前Listener对象,这个属性有点鸡肋,一般写topic名称和group名称
//的时候都是写死的,如果需要动态配置,那可以通过spring的占位符机制配合配置中心实现。beanRef的作用就是在当前listener对象中设置几个属性,然后在KafkaListener注解中
//的值可以通过#{beanRef.属性值}去获取,看似实现了动态设置topic或者group等属性,但为什么不直接使用spring的上下文获取,直接${}
String beanRef = kafkaListener.beanRef();
if (StringUtils.hasText(beanRef)) {
this.listenerScope.addListener(beanRef, bean);
}
endpoint.setBean(bean);
endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory);
//默认是org.springframework.kafka.KafkaListenerEndpointContainer#+endpoint计数器的数字
endpoint.setId(getEndpointId(kafkaListener));
//从注解中获取groupId,如果groupId没有设置则获取id,如果都没设置,则返回null
endpoint.setGroupId(getEndpointGroupId(kafkaListener, endpoint.getId()));
endpoint.setTopicPartitions(resolveTopicPartitions(kafkaListener));
endpoint.setTopics(resolveTopics(kafkaListener));
endpoint.setTopicPattern(resolvePattern(kafkaListener));
endpoint.setClientIdPrefix(resolveExpressionAsString(kafkaListener.clientIdPrefix(), "clientIdPrefix"));
String group = kafkaListener.containerGroup();
if (StringUtils.hasText(group)) {
Object resolvedGroup = resolveExpression(group);
if (resolvedGroup instanceof String) {
endpoint.setGroup((String) resolvedGroup);
}
}
String concurrency = kafkaListener.concurrency();
if (StringUtils.hasText(concurrency)) {
endpoint.setConcurrency(resolveExpressionAsInteger(concurrency, "concurrency"));
}
String autoStartup = kafkaListener.autoStartup();
if (StringUtils.hasText(autoStartup)) {
endpoint.setAutoStartup(resolveExpressionAsBoolean(autoStartup, "autoStartup"));
}
resolveKafkaProperties(endpoint, kafkaListener.properties());
//KafkaListenerContainerFactory对象的设置顺序为,优先从@KafkaListener中开发指定的去获取,如果找不到,获取当前KafkaListenerEndpointDescriptor对象中设置的值
//其实KafkaListenerEndpointDescriptor这个对象中的值也是从注解或者配置文件中拿到的,如果都没有的话,获取默认的值,默认的值为
//KafkaListenerAnnotationBeanPostProcessor的afterSingletonsInstantiated中设置的defaultContainerFactoryBeanName即ConcurrentKafkaListenerContainerFactory,
//这个factory对象是为了创建listenercontainer对象用的,也是和endpoint对象一一对应的,我们未来可以自定义扩展的很多功能都是操作需要这些container对象,这些container对象也是
//被放在了spring上下文中可以很方便的进行获取,container对象也是spring对kafka-client封装的时候核心对象,所有的元信息都存储在这里
KafkaListenerContainerFactory<?> factory = null;
String containerFactoryBeanName = resolve(kafkaListener.containerFactory());
if (StringUtils.hasText(containerFactoryBeanName)) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
try {
factory = this.beanFactory.getBean(containerFactoryBeanName, KafkaListenerContainerFactory.class);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanInitializationException("Could not register Kafka listener endpoint on [" + adminTarget
+ "] for bean " + beanName + ", no " + KafkaListenerContainerFactory.class.getSimpleName()
+ " with id '" + containerFactoryBeanName + "' was found in the application context", ex);
}
}
endpoint.setBeanFactory(this.beanFactory);
String errorHandlerBeanName = resolveExpressionAsString(kafkaListener.errorHandler(), "errorHandler");
if (StringUtils.hasText(errorHandlerBeanName)) {
endpoint.setErrorHandler(this.beanFactory.getBean(errorHandlerBeanName, KafkaListenerErrorHandler.class));
}
//将上述创建的endpoint注册到KafkaListenerEndpointRegistrar中去,其实就是为KafkaListenerEndpointRegistrar这个bean的endpointDescriptors对象添加元素,具体注册的触发是在
//KafkaListenerEndpointRegistry类中的start方法中,最终会调用registerListenerContainer方法
this.registrar.registerEndpoint(endpoint, factory);
if (StringUtils.hasText(beanRef)) {
this.listenerScope.removeListener(beanRef);
}
}
//KafkaListenerEndpointRegistry和KafkaListenerEndpointRegistrar这两个类的名字很像,太蛋疼了。。可以简单理解为KafkaListenerEndpointRegistrar是
//KafkaListenerEndpointRegistry的管理者或者调用者吧
public void registerEndpoint(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory) {
Assert.notNull(endpoint, "Endpoint must be set");
Assert.hasText(endpoint.getId(), "Endpoint id must be set");
// Factory may be null, we defer the resolution right before actually creating the container
KafkaListenerEndpointDescriptor descriptor = new KafkaListenerEndpointDescriptor(endpoint, factory);
synchronized (this.endpointDescriptors) {
if (this.startImmediately) {
//startImmediately参数默认是false,所以从上述的逻辑流转下来不会进入这里,具体注册的触发是在KafkaListenerEndpointRegistrar类中的registerAllEndpoints方法中,
//最终会调用registerListenerContainer方法
//调用endpoint注册器注册当前endpoint,注册器采用默认的设置,在KafkaBootstrapConfiguration类中创建的bean
this.endpointRegistry.registerListenerContainer(descriptor.endpoint,
resolveContainerFactory(descriptor), true);
}
else {
this.endpointDescriptors.add(descriptor);
}
}
}
registerListenerContainer(一个endpoint对应一个container)
public void registerListenerContainer(KafkaListenerEndpoint endpoint, KafkaListenerContainerFactory<?> factory,
boolean startImmediately) {
Assert.notNull(endpoint, "Endpoint must not be null");
Assert.notNull(factory, "Factory must not be null");
//spring会自动生成(如果没配置的话,这个id会被用作map的key使用)
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 + "'");
//从factory创建一个MessageListenerContainer,如果注解或者配置中指定了factory,则会使用指定的,如果没指定会使用默认的factory
//(ConcurrentKafkaListenerContainerFactory),最终会创建一个ConcurrentMessageListenerContainer对象,上述注释中提到的重要对象
//并且会将一些客户端配置的参数初始化和从注解和配置文件中获取到的参数也设置进此对象,配置文件中参数的获取是利用springboot的自动装配机制完成的,可见KafkaAutoConfiguration类
MessageListenerContainer container = createListenerContainer(endpoint, factory);
//这个参数listenerContainers我们可以通过拿到当前的registor对象通过他提供的api获取到所有的consumer container对象,这样我们就可以做一些自定义的扩展功能,比如我可以去停止某个
//某个container对象对某个topic的消费
this.listenerContainers.put(id, container);
if (StringUtils.hasText(endpoint.getGroup()) && this.applicationContext != null) {
List<MessageListenerContainer> containerGroup;
if (this.applicationContext.containsBean(endpoint.getGroup())) {
containerGroup = this.applicationContext.getBean(endpoint.getGroup(), List.class);
}
else {
//如果每个endpoint设置了group属性,会将上述创建的container对象注册到spring上下文中,beanname为group的值
//一个endpoint和一个@kafkaListener注解是一一对应的,一个endpoint和一个container也是一一对应的,每个@kafkalistener注解都可以设置group属性,不同
//的注解设置的group属性可以想同,如果相同,会在这里进行体现,spring kafka会注册container的bean,beanname为group,value为同一个group的不同endpoint,
//或者同一个group的不通同container,具体注册这个有什么用,我目前还不知道,看到了会补充。但是最起码可以从spring容器中拿到这些bean,只不过几乎没人去写这个group
//的值,请注意,这个group的值和groupid不是一个东西,不要混淆了。
containerGroup = new ArrayList<MessageListenerContainer>();
this.applicationContext.getBeanFactory().registerSingleton(endpoint.getGroup(), containerGroup);
}KafkaListenerEndpointRegistrar
containerGroup.add(container);
}
if (startImmediately) {
//注意看这里,会调用ConcurrentMessageListenerContainer的dostart方法
startIfNecessary(container);
}
}
}
protected MessageListenerContainer createListenerContainer(KafkaListenerEndpoint endpoint,
KafkaListenerContainerFactory<?> factory) {
//工厂对象创建container对象,下面会详细分析
MessageListenerContainer listenerContainer = factory.createListenerContainer(endpoint);
if (listenerContainer instanceof InitializingBean) {
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;
}
//创建listennerContainer
public C createListenerContainer(KafkaListenerEndpoint endpoint) {
//此方法把@kafkaListener注解中设置的属性值放在container对象中,并且把autoconfigiration中获取到的配置文件配置的属性值的factory也放置在container中,
//返回一个ConcurrentMessageListenerContainer对象(我们这里只说默认的,不说自定义的),这一步不管使用默认的还是自定义的本质上都是要把配置文件和注解中开发定义的参数放在container
//对象中,返回的这个container目前已经有所有的kafka consumer的配置的参数了
C instance = createContainerInstance(endpoint);
if (endpoint.getId() != null) {
//container设置一个beanname,为开发设置的id或者spring自动生成的,上面有提到,后续这个container要注册到spring上下文中去的。
instance.setBeanName(endpoint.getId());
}
if (endpoint instanceof AbstractKafkaListenerEndpoint) {
//一定是true,这一步是利用springboot的自动装配原理,配置了一下enpoint的一些特殊处理的类,比如消息转换器、消息过滤器、重试策略等等,这些均可以自定义,
//自定义的方式就是上面说的自己定义factory对象,不要使用springboot默认的ConcurrentKafkaListenerContainerFactory。如果没定义,默认的配置可以
//阅读KafkaAnnotationDrivenConfiguration和KafkaAnnotationDrivenConfiguration类的代码。这里不再赘述。
configureEndpoint((AbstractKafkaListenerEndpoint<K, V>) endpoint);
}
//设置container和handlermethod绑定,handlermethod就是业务开发需要编写的消费消息的代码体(感觉和Springmvc的controller一样)
endpoint.setupListenerContainer(instance, this.messageConverter);
//设置container对象的一些特殊处理的类,比如消息转换器、消息过滤器、重试策略等等,和上面的设置endpoint的这些类是一样的,都是通过自动装配配置的
//个人觉得这块逻辑很奇怪,在上面的代码中把这些属性设置到了endpoint对象中,然后又在下面这个方法中从endpoint中把这些属性拿出来设置到container对象中,为什么不在一个方法中都设置了。
initializeContainer(instance, endpoint);
//到目前为止,配置文件的配置、注解的配置、消费处理的bean、消费的方法、还有自动装配的默认配置都已经设置好了,都在这个container对象中了,接下来就是看具体怎么去消费的。
return instance;
}
dostart()
protected void doStart() {
if (!isRunning()) {
//创建一个kafka-client原生自带的KafkaConsumer对象,通过此对象和kafka集群通信,调用partitionsFor方法,检查当前container中的topic在当前kafka中存在与否,不存在则抛异常
checkTopics();
ContainerProperties containerProperties = getContainerProperties();
TopicPartitionInitialOffset[] topicPartitions = containerProperties.getTopicPartitions();
//当前并发消费数如果大于指定分区数量的话,将当前并发消费数设置为和分区数相等,这点应该是为了保证顺序消费和资源利用率
if (topicPartitions != null && this.concurrency > topicPartitions.length) {
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);
for (int i = 0; i < this.concurrency; i++) {
KafkaMessageListenerContainer<K, V> container;
if (topicPartitions == null) {
container = new KafkaMessageListenerContainer<>(this, this.consumerFactory, containerProperties);
}
else {
container = new KafkaMessageListenerContainer<>(this, this.consumerFactory,
containerProperties, partitionSubset(containerProperties, i));
}
//beanname为上述解释的id的值,默认spring 会生成,一定不为null
String beanName = getBeanName();
container.setBeanName((beanName != null ? beanName : "consumer") + "-" + i);
if (getApplicationEventPublisher() != null) {
container.setApplicationEventPublisher(getApplicationEventPublisher());
}
container.setClientIdSuffix("-" + i);
container.setGenericErrorHandler(getGenericErrorHandler());
container.setAfterRollbackProcessor(getAfterRollbackProcessor());
container.setRecordInterceptor(getRecordInterceptor());
container.setEmergencyStop(() -> {
stop(() -> {
// NOSONAR
});
publishContainerStoppedEvent();
});
if (isPaused()) {
container.pause();
}
//调用上述KafkaMessageListenerContainer的start方法
container.start();
this.containers.add(container);
}
}
}
KafkaMessageListenerContainer类的start
protected void doStart() {
if (isRunning()) {
return;
}
if (this.clientIdSuffix == null) {
//创建一个kafka-client原生自带的KafkaConsumer对象,通过此对象和kafka集群通信,调用partitionsFor方法,检查当前container中的topic在当前kafka中存在与否,不存在则抛异常
checkTopics();
}
ContainerProperties containerProperties = getContainerProperties();
//检查ack的count和time的设置(无需关注这里)
checkAckMode(containerProperties);
Object messageListener = containerProperties.getMessageListener();
Assert.state(messageListener != null, "A MessageListener is required");
if (containerProperties.getConsumerTaskExecutor() == null) {
//创建异步任务处理器,这里不是线程池,从代码上来看,这里是每一个enpoint(即每一个container)都会创建用户设置的并发消费数个线程
SimpleAsyncTaskExecutor consumerExecutor = new SimpleAsyncTaskExecutor(
(getBeanName() == null ? "" : getBeanName()) + "-C-");
containerProperties.setConsumerTaskExecutor(consumerExecutor);
}
Assert.state(messageListener instanceof GenericMessageListener, "Listener must be a GenericListener");
GenericMessageListener<?> listener = (GenericMessageListener<?>) messageListener;
ListenerType listenerType = deteremineListenerType(listener);
//此处为最最核心的方法,每个endpoint/container启动一个线程死循环去pull消息然后交由listener去一一或批量处理
this.listenerConsumer = new ListenerConsumer(listener, listenerType);
setRunning(true);
//启动当前线程
this.listenerConsumerFuture = containerProperties
.getConsumerTaskExecutor()
.submitListenable(this.listenerConsumer);
}