@RefreshScope 注解引起的kafka无法消费问题排查

使用公司内部封装的 canal 数据同步工具从kafka消费数据时,@RefreshScope 注解引起的无法消费问题排查

问题代码展示:

public class CanalListenerAnnotationBeanPostProcessor implements
    BeanPostProcessor, SmartInitializingSingleton, BeanFactoryPostProcessor {
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        if (notAnnotatedClasses.contains(bean.getClass())) return bean;
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        // 只扫描类的方法,目前 CanalListener 只支持在方法上
        Map<Method, CanalListener> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                                                                                       (MethodIntrospector.MetadataLookup<CanalListener>) method -> findListenerAnnotations(method));
        // ... 省略其它代码
        annotatedMethods.entrySet().stream()
            .filter(e -> e != null)
            .forEach(ele -> registrars.add(new CanalListenerEndpointRegistrar(bean, ele)));
        // ... 省略其它代码
        return bean;
    }
    
    public void afterSingletonsInstantiated() {
        // ... 省略其它代码
        TransponderContainerFactory.registerListenerContainer(configurableListableBeanFactory, canalProperties, registrars, skip);
    }
}
// afterSingletonsInstantiated 方法中调用该方法
private static void registerTransponderContainer(
            String destination, CanalProperties.EndpointInstance endpointInstance,
            ConfigurableListableBeanFactory beanFactory, List<CanalListenerEndpointRegistrar> registrarList, Skip skip) {
        KafkaCanalConnector connector = CanalConnectorFactory.createConnector(destination, endpointInstance);
        DmlMessageTransponderContainer singletonObject = 
            new DmlMessageTransponderContainer(connector, registrarList, endpointInstance, skip);
        beanFactory.registerSingleton(getContainerID(destination), singletonObject);
    }

public class DmlMessageTransponderContainer extends AbstractCanalTransponderContainer {}
public abstract class AbstractCanalTransponderContainer implements SmartLifecycle {
    public void start() {
        new Thread(() -> {
            // ...
            initConnect(); // 建立kafka链接,创建 KafkaConsumer
            // ...
        }).start();
        setRunning(true);
    }
}

通过上述源码我们可以看到 CanalListenerAnnotationBeanPostProcessor 实现了 BeanPostProcessor(简称 BPP) 和 SmartInitializingSingleton 这两个接口,了解 spring 的同学应该知道 spring Bean 在完成实例化后会回调 BeanPostProcessor 接口中的方法(每个对象实例化时都会回调),但是回调也是有条件限制的,后面的 spring 源码中我们会看到;但是对于 SmartInitializingSingleton 接口只会在所有单例Bean 都实例化完成后回调一次,记住 是单例Bean

AbstractCanalTransponderContainer 对象 实现了 SmartLifecycle 生命周期接口,该接口在什么时候回调,在 spring 上下文刷新完成后进行回调(后面我们看源码)。

被 @RefreshScope 注解标注的类何时实例化呢?首先我们要知道在 spring 中有几种作用域,比如 request、session、scope等,而singleton 也属于一种特殊的对象域,在 spring 中对象的默认作用域就是 singleton 单例模式的,这类对象保存在 singletonObjects 集合中。而对于 @RefreshScope 注解标注的类 将被缓存在 scopes 中,这类对象不属于单例对象。

下面我们就来看看 spring 源码的具体实现逻辑:

spring 类扫描流程请查看 ClassPathBeanDefinitionScanner#doScan()方法,这里就不去讲解具体扫描代码了,只需要大家知道在扫描到带有 @RefreshScope 注解时会对该 BeanDefinition 做一些特殊处理,如下述内容。

**ScopedProxyUtils 类:**下述方法中我们看到 @RefreshScope 注解标注的类 在BDMap 中将会存在两个Bean定义信息:

一个名称是 原始类名,其 synthetic = true,scope=singleton,其绑定的 BD 是 proxyDefinition 代理Bean 定义信息

另一个名称是 scopedTarget.原始类名,其 synthetic = false , scope = refresh(非单例),其绑定的 BD 是原始定义信息

public abstract class ScopedProxyUtils {

    private static final String TARGET_NAME_PREFIX = "scopedTarget.";
    
    public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
			BeanDefinitionRegistry registry, boolean proxyTargetClass) {

		String originalBeanName = definition.getBeanName();
		BeanDefinition targetDefinition = definition.getBeanDefinition();
		String targetBeanName = getTargetBeanName(originalBeanName);
		
		RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
		proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
		proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
		// ...
		// 将目标 Bean 定义信息注册到 BDMap 中, targetDefinition 原始 BD(Bean 定义信息) 信息
		registry.registerBeanDefinition(targetBeanName, targetDefinition);
		// 返回的是 代理 proxyDefinition 定义信息,也会被注册到 BDMap 中
		return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
	}
}

**GenericScope 类:**我们看到该类实现了 BeanFactoryPostProcessor 接口,在spring 刷新上下文的过程中 AbstractApplicationContext.refresh()中有一步是对所有实现了 BeanFactoryPostProcessor 接口的回调。在这个过程中就会回调该类,通过源码我们看到会对 @RefreshScope 注解标注的类的 BeanDefinition 信息设置 synthetic = true,该属性在Bean实例化过程中将会控制对 BPP 接口的回调。

public class GenericScope implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, 								DisposableBean {
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
        throws BeansException {
        for (String name : registry.getBeanDefinitionNames()) {
            BeanDefinition definition = registry.getBeanDefinition(name);
            if (definition instanceof RootBeanDefinition) {
                RootBeanDefinition root = (RootBeanDefinition) definition;
                if (root.getDecoratedDefinition() != null && root.hasBeanClass()
                    && root.getBeanClass() == ScopedProxyFactoryBean.class) {
                    if (getName().equals(root.getDecoratedDefinition().getBeanDefinition().getScope())) {
                        // ...
                        root.setSynthetic(true);
                    }
                }
            }
        }
    }
}

对象实例化过程中 BeanPostProcessor 和 SmartInitializingSingleton 接口的回调流程

BeanPostProcessor 回调

AbstractAutowireCapableBeanFactory#initializeBean 方法,方法调用链:doCreateBean() —> initializeBean()

通过下述源码我们看到 spring 在创建bean的过程中回调了各种接口方法来对bean进行额外配置,通过这些接口我们可以对bean做额外配置

1、Aware 接口回调,通过Aware 接口我们可以获取到 bean 工厂本身

2、回调 BPP 的 postProcessBeforeInitialization 方法

3、回调 初始化方法,如 afterPropertiesSet(),用户自定义的 initMethod

4、回调 BPP 的 postProcessAfterInitialization 方法

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
   // ...
    invokeAwareMethods(beanName, bean);
   Object wrappedBean = bean;
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   }
   // ...
   invokeInitMethods(beanName, wrappedBean, mbd);
    
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
   }
   return wrappedBean;
}

SmartInitializingSingleton 回调

方法调用链:AbstractApplicationContext#refresh() —> AbstractApplicationContext#finishBeanFactoryInitialization方法 —> DefaultListableBeanFactory#preInstantiateSingletons 方法

通过源码我们发现 SmartInitializingSingleton 的回调实在所有 bean 实例化之后进行的。

public void preInstantiateSingletons() throws BeansException {
   // ...
   List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
   // 实例化所有单例非懒加载的 bean 
   for (String beanName : beanNames) {
      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
      if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
          // ...
         getBean(beanName);
      }
   }
   // 循环遍历所有单例 bean 检测是否是 SmartInitializingSingleton 实例,然后回调
   for (String beanName : beanNames) {
      Object singletonInstance = getSingleton(beanName);
      if (singletonInstance instanceof SmartInitializingSingleton) {
         final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
         // ...
          smartSingleton.afterSingletonsInstantiated();
      }
   }
}

SmartLifecycle 回调

方法调用链:AbstractApplicationContext#refresh() —> finishRefresh() 方法 —> DefaultLifecycleProcessor#onRefresh() —> startBeans()

finishRefresh() 方法在 AbstractApplicationContext#refresh() 最后调用,也即在 spring 完成所有单例bean创建后调用。在该方法中我们注意到 spring 发出了一个 上下文刷新完成的 事件 ContextRefreshedEvent 事件。

protected void finishRefresh() {
   // ...
   // SmartLifecycle 回调
   getLifecycleProcessor().onRefresh();
   // 发布上下文刷新完成事件,
   publishEvent(new ContextRefreshedEvent(this));
   // ...
}

private void startBeans(boolean autoStartupOnly) {
    Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
    Map<Integer, LifecycleGroup> phases = new HashMap<>();
    lifecycleBeans.forEach((beanName, bean) -> {
        if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
            // ...
            phases.put(phase, group);
        }
    });
    if (!phases.isEmpty()) {
        // ...
        for (Integer key : keys) {
            phases.get(key).start(); // 调用 SmartLifecycle 的 start 方法
        }
    }
}

spring-cloud-context 会监听到 spring 发出的 ContextRefreshedEvent 事件,在该事件的处理过程中才会对 @RefreshScope 注解标注的类进行初始化,被 @RefreshScope 注解标注的类在生成 BeanDefiniton 的时候其 scope 的 属性将被设置为 refresh,非单例对象。

public class RefreshScope extends GenericScope implements ApplicationContextAware,
ApplicationListener<ContextRefreshedEvent>, Ordered {
    
    public void onApplicationEvent(ContextRefreshedEvent event) {
		start(event);
	}

	public void start(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.context && this.eager && this.registry != null) {
			eagerlyInitialize();
		}
	}
	
	private void eagerlyInitialize() {
		for (String name : this.context.getBeanDefinitionNames()) {
			BeanDefinition definition = this.registry.getBeanDefinition(name);
             // 实例化 scope = "refresh" 的所有bean对象
			if (this.getName().equals(definition.getScope()) && !definition.isLazyInit()) {
				Object bean = this.context.getBean(name);
				if (bean != null) {
					bean.getClass();
				}
			}
		}
	}
}

通过上述源码分析,我们可以了解到 @RefreshScope 注解标注的类的实例化是由 spring cloud 负责实例化的,在这一步之前spring上下文已经完成了所有单例bean的实例化(完成了 bean 的生命周期接口的回调)。

回到我们的问题上来,由于 SmartInitializingSingleton 接口是单例bean实例化完成后进行回调的接口,而 @RefreshScope 注解标注的类为非单例bean,所以不回回调该接口的方法。而我们的kafka的消费者的创建过程却放在了该接口的回调中实现,所以对非单例bean不生效。

这就是该问题分析排查的整个过程,希望对大家有所帮助。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值