MetaQ启动失败引发对ConditionalOnProperty的思考

255 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

问题背景

灰度环境不允许配置消息消费,由于项目很多,每个都要修改配置增加上产、灰发两个维度的配置工作量会比较大。于是使用中间件的方式,根据环境判断,如果是灰度环境则不启动消费者。操作动作如下:

  1. 如果存在消费者bean则添加@Conditional(EnvCondition.class)注解,EnvCondition判断是否灰度环境,如果是则不注册bean
  2. 增加MetaqEnvironmentPostProcessor后置处理,判断如果是灰度环境则删除环境中的spring.ons.consumer前缀的配置项

线上发布了几个项目均正常生效后继续发布第二批,结果抛出了如下异常(日志有删减)。扎心了,为啥还是注册并启动了消费者bean

Wed Jun 10 13:59:39 CST 2020 dpath's ModuleClassLoader JM.Log:INFO Set dpath log path: /home/admin/logs/dpath
env gray ons consumer not register
remove config keyspring.ons.consumer.consume-thread-nums
remove config keyspring.ons.consumer.consumer-id
remove config keyspring.ons.consumer.mq-type
...
Wed Jun 10 13:59:49 CST 2020 config-client's ModuleClassLoader JM.Log:INFO Set configclient log path: /home/admin/logs/configclient
Stopping available components
java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.....pandora.boot.loader.MainMethodRunner.run(MainMethodRunner.java:54)
        at com.....pandora.boot.loader.Launcher.launch(Launcher.java:87)
        at com.....pandora.boot.loader.Launcher.launch(Launcher.java:50)
        at com.....pandora.boot.loader.SarLauncher.main(SarLauncher.java:171)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'onsConsumerBean' defined in class path resource [com/.../boot/ons/ConsumerAutoConfigure.class]: Invocation of init method failed; nested exception is com.....openservices.ons.api.exception.ONSClientException: Please Input ConsumerId
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1630)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
		at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:756)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)
        at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:123)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:666)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:353)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:300)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1082)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1071)
        at com.....order.image.Application.main(Application.java:21)
        ... 8 more
Caused by: com.....openservices.ons.api.exception.ONSClientException: Please Input ConsumerId
        at com.....openservices.ons.api.impl.ONSFactoryNotifyAndMetaQImpl.findMQTypeOfConsumer(ONSFactoryNotifyAndMetaQImpl.java:144)
        at com.....openservices.ons.api.impl.ONSFactoryNotifyAndMetaQImpl.createConsumer(ONSFactoryNotifyAndMetaQImpl.java:113)
        at com.....openservices.ons.api.ONSFactory.createConsumer(ONSFactory.java:167)
        at com.....openservices.ons.api.bean.ConsumerBean.start(ConsumerBean.java:50)
		at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1759)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1696)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1626)
        ... 24 more

问题分析

查看MetaQ自动配置,配置有三类

  1. 单消费者配置,通过@bean注解注册,前缀为:spring.ons.consumer
  2. 单消费者顺序配置,通过@bean注解注册,前缀为:spring.ons.order.consumer
  3. 多消费者配置,通过MultiConsumersRegistrar注册选择器注册,前缀为:前两者,key均以s负数形式结尾

为什么个别服务没问题

检查发现其他服务因为配置了多个消费者所以走的MultiConsumersRegistrar配置。并且可以从代码中看到多消费者配置是从environment环境中获取的配置,如果环境中配置被删除则不会走到注册bean的代码块就不会有异常。启动日志中可以看到已经将环境中的配置成功移除,所以不会有问题。

@Configuration
@ConditionalOnProperty(name = OnsConstants.ENABLED, matchIfMissing = true)
public static class MultiConsumersRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware, BeanPostProcessor {
    private static final Logger logger = LoggerFactory.getLogger(MultiConsumersRegistrar.class);

    private ConfigurableEnvironment environment;
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        innerRegisterBeanDefinitions(registry, new GeneralConsumerBeanInfo());
        innerRegisterBeanDefinitions(registry, new OrderConsumerBeanInfo());
    }

    private void innerRegisterBeanDefinitions(BeanDefinitionRegistry registry, ConsumerBeanInfo consumerBeanInfo){
        MultiConsumerProperties properties = BinderUtils.bind(environment, consumerBeanInfo.getPropertiesPrefix(), MultiConsumerProperties.class);

        for (String beanName : properties.getConsumerProperties().keySet()) {
            BeanDefinition beanDefinition = consumerBeanInfo.createConsumerBeanDefinition(
                properties.getConsumerProperties().get(beanName));
            ...
            registry.registerBeanDefinition(beanName, beanDefinition);
            logger.info("register ONS consumer bean {}. Bean:{}", new Object[]{ beanName, consumerBeanInfo});
        }
    }
	...
}

为什么个别服务会有问题

推断1:我们删除了环境中的消费者配置。但是如果ConditionalOnProperty注解不是根据环境中配置判断的条件则会有问题

查看MetaQ自动配置,异常的是consumerBean该方法注册的bean初始化过程抛出的。发现该类型的配置是通过判断是否存在spring.ons.consumer.consumer-id配置决定是否要注册bean。我们删除了环境中的消费者配置。但是如果ConditionalOnProperty注解不是根据环境中配置判断的条件则会有问题,因为我们实际的properties配置中是存在消费者配置的。源码看一波,走起

@Primary
@Bean(name = OnsConstants.CONSUMER_NAME, initMethod = OnsConstants.INIT_METHOD, destroyMethod = OnsConstants.DESTROY_METHOD)
@ConditionalOnProperty(name = OnsConstants.CONSUMER_PREFIX + ".consumer-id")
public ConsumerBean consumerBean() {
    if (properties != null) {
        logger.info("register ONS consumer bean {}.", OnsConstants.CONSUMER_NAME);
        ConsumerBean consumerBean = new ConsumerBean();
        consumerBean.setProperties(properties.getProperties());
        // 设置一个空的map,让start函数可以顺利执行
        consumerBean.setSubscriptionTable(new HashMap<Subscription, MessageListener>());
        return consumerBean;
    }
    return null;
}

ConditionalOnProperty注解实现原理

通过@Bean方式注册bean流程回顾

  1. ConfigurationClassParser.doProcessConfigurationClass检索并构建BeanMethod元数据,添加至其对应的sourceClass:ConsumerAutoConfigure
  2. ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass读取bean定义加载至上下文
  3. 加载bean注解方式的bean定义ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod
  4. ConditionEvaluator.shouldSkip判断bena是否通过condition标记为跳过,如果是则跳过
  5. 判断阶段是否匹配,阶段为两类ConfigurationCondition:ConfigurationPhase._PARSE_CONFIGURATION(解析Configuration类的条件注解并判断是否需要处理),_ConfigurationPhase.REGISTER_BEAN(解析SourceClass类中BeanMethod方法定义的bean的条件注解并判断是否需要处理)
  6. ConditionEvaluator.shouldSkip根据metadata元数据获取conditions
  7. ConditionEvaluator.shouldSkip遍历conditions条件类回调matches判断是否匹配

了解条件注解的处理逻辑后我们看下该条件对应的条件逻辑类:OnPropertyCondition

OnPropertyCondition

  1. 调用父类SpringBootCondition.matches
  2. 回调子类OnPropertyCondition.getMatchOutcome

resolver是从条件上下文中获取的context.getEnvironment()。根据环境判断是否匹配。条件上下文是matches方法的入参。查看入参的来源为ConditionEvaluator实例的context。ConditionEvaluator实例context来源自ConfigurationClassParser的构造器入参。ConfigurationClassParser的构造器中入参来源自ConfigurationClassPostProcessor.environment(实现EnvironmentAware接口获取环境实例)

推断1不成立

因为二者均是通过Environment环境判断是否需要注册bean,但是结果却不同。因此推断1不成立

总结

  1. Condiation与ConfigurationCondition的区别在于后者可以指定条件生效阶段,前者是两个阶段均生效。
  2. 多个条件判断是与的关系,只要有一个条件不符合就会跳过注册
  3. 条件顺序通过AnnotationAwareOrderComparator实现

又是一个不解之谜,本地也无法复现,目前无法断定问题的原因。很伤心,不过排查过程温习了老的知识,也同时学到了很多新知识点。温故而知新吧^_^!!!