本文已参与「新人创作礼」活动,一起开启掘金创作之路。
问题背景
灰度环境不允许配置消息消费,由于项目很多,每个都要修改配置增加上产、灰发两个维度的配置工作量会比较大。于是使用中间件的方式,根据环境判断,如果是灰度环境则不启动消费者。操作动作如下:
- 如果存在消费者bean则添加@Conditional(EnvCondition.class)注解,EnvCondition判断是否灰度环境,如果是则不注册bean
- 增加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自动配置,配置有三类
- 单消费者配置,通过@bean注解注册,前缀为:spring.ons.consumer
- 单消费者顺序配置,通过@bean注解注册,前缀为:spring.ons.order.consumer
- 多消费者配置,通过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流程回顾
- ConfigurationClassParser.doProcessConfigurationClass检索并构建BeanMethod元数据,添加至其对应的sourceClass:ConsumerAutoConfigure
- ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass读取bean定义加载至上下文
- 加载bean注解方式的bean定义ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod
- ConditionEvaluator.shouldSkip判断bena是否通过condition标记为跳过,如果是则跳过
- 判断阶段是否匹配,阶段为两类ConfigurationCondition:ConfigurationPhase._PARSE_CONFIGURATION(解析Configuration类的条件注解并判断是否需要处理),_ConfigurationPhase.REGISTER_BEAN(解析SourceClass类中BeanMethod方法定义的bean的条件注解并判断是否需要处理)
- ConditionEvaluator.shouldSkip根据metadata元数据获取conditions
- ConditionEvaluator.shouldSkip遍历conditions条件类回调matches判断是否匹配
了解条件注解的处理逻辑后我们看下该条件对应的条件逻辑类:OnPropertyCondition
OnPropertyCondition
- 调用父类SpringBootCondition.matches
- 回调子类OnPropertyCondition.getMatchOutcome
resolver是从条件上下文中获取的context.getEnvironment()。根据环境判断是否匹配。条件上下文是matches方法的入参。查看入参的来源为ConditionEvaluator实例的context。ConditionEvaluator实例context来源自ConfigurationClassParser的构造器入参。ConfigurationClassParser的构造器中入参来源自ConfigurationClassPostProcessor.environment(实现EnvironmentAware接口获取环境实例)
推断1不成立
因为二者均是通过Environment环境判断是否需要注册bean,但是结果却不同。因此推断1不成立
总结
- Condiation与ConfigurationCondition的区别在于后者可以指定条件生效阶段,前者是两个阶段均生效。
- 多个条件判断是与的关系,只要有一个条件不符合就会跳过注册
- 条件顺序通过AnnotationAwareOrderComparator实现
又是一个不解之谜,本地也无法复现,目前无法断定问题的原因。很伤心,不过排查过程温习了老的知识,也同时学到了很多新知识点。温故而知新吧^_^!!!