网易考拉海购Spring Boot踩坑实录

社区编辑2018-05-17 18:27

在《微服务实践:网易考拉海购Dubbok框架优化详解》一文中,我们系统地介绍了网易考拉海购(下文简称“考拉”)对Dubbo的优化工作。实际上,考拉在微服务实践中同时也尝试了Spring Boot框架。最近团队在mykaola工程接入静态配置托管Dubbo配置时(注:本文中Dubbo特指考拉优化过的Dubbok),遇到了一个Spring Boot的坑,本文把定位过程和解决方案记录并分享,希望网易云的小伙伴们不要再次踩到同样的坑,并浪费大量精力和时间排查问题。

背景

mykaola工程使用Spring Boot框架,Spring版本号4.1.1,Spring Boot版本号1.1.9,spring-security版本号4.0.2,启用了Spring Boot框架的Anto Configuration特性。

 

Dubbo配置disconf静态配置托管优化,由一个StaticConfigPropertiesFactoryBean来读取静态配置。


Dubbo配置直接引用这个Properties的值。如下所示,Dubbo接口的group通过disconf的静态配置项dubbo.hst.pay.group定义:


StaticConfigPropertiesFactoryBean这个bean的configSrc属性值是个占位符。但是启动工程时出现了一个问题,抛出了一个IllegalArgumentException异常,异常日志如下:

Caused by: java.lang.IllegalArgumentException: configSrc error : ${disconf.configSrc}

at com.baidu.disconf.client.xxxxxx.properties.StaticConfigPropertiesFactoryBean.createProperties(StaticConfigPropertiesFactoryBean.java:65)

at org.springframework.beans.factory.config.PropertiesFactoryBean.afterPropertiesSet(PropertiesFactoryBean.java:71)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)

… 35 more

从日志中看到configSrc这个属性的值不满足条件导致抛出了这个异常,而且值${disconf.configSrc},这里比较奇怪,在初始化bean之前框架应该会对占位符进行解析求值处理的,什么原因导致占位符未被处理bean就开始初始化了?

异常分析

 

Spring框架由PropertySourcesPlaceholderConfigurer来处理占位符,PropertySourcesPlaceholderConfigurer是一个BeanFactoryPostProcessor,在容器所有BeanDefinition被加载之后,bean初始化之前,BeanFactoryPostProcessor会被激活,PropertySourcesPlaceholderConfigurer会加载所有被引入的属性文件,并且查找占位符对应的值,并把对应的值设置到bean属性对应的PropertyValue中,这样在bean初始化时设置每个属性的值时设置的就是处理后的值了。

 

org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List<BeanFactoryPostProcessor>)


就是这在触发这个BeanFactoryPostProcessor的,注意代码行数是162行。

但是现在的现象是configSrc这个属性初始化之后却是未处理的占位符字符串。分析原因有两种可能,第一种disconf.configSrc这个配置项在文件中未定义或未被容器加载,第二种StaticConfigPropertiesFactoryBean在属性占位符处理之前就初始化了。第一种原因很容易就排除了,disconf.configSrc这个配置项已经定义了,在做这次修改之前就已存在之前一直都是没问题的,且由于ignoreUnresolvablePlaceholders这个设置默认是false,就算它未被定义容器也会提前抛出IllegalArgumentException异常:

org.springframework.util.PropertyPlaceholderHelper.parseStringValue(String, PlaceholderResolver, Set<String>)


所以只能是第二种原因了,那就是StaticConfigPropertiesFactoryBean在属性占位符处理之前就被容器初始化了。Spring框架如此庞大,要找原因最有效的手段就是debug了,把断点打到StaticConfigPropertiesFactoryBean初始化代码处,看它是从哪进来的。

org.springframework.util.PropertyPlaceholderHelper.parseStringValue(String, PlaceholderResolver, Set<String>)

通过debug发现,这个bean的初始化也是从BeanFactoryPostProcessor调进去的,而且看下代码是94行,在占位符处理的164行前面,好了,这里可以解释configSrc值为什么是错的。但是这些BeanFactoryPostProcessor是干嘛的?为什么在其它BFPP激活之前就去初始化bean了。看一下这个BFPP是什么:


这个BFPP是ConfigurationClassPostProcessor,当应用使用了annotation-config或component-scan扫描注解bean时,容器会自动注册一个ConfigurationClassPostProcessor来加载注解定义的BeanDefinition,按常理,这个BFPP也只会生成BeanDefinition,不会执行bean初始化动作。那么是什么地方导致bean发生初始化了呢?再看下上面哪个调用栈,在OnMissingBeanCondition.matches之后就调用BeanFactory的getBeansXXX了,然后一步一步触发了createBean。那这个OnMissingBeanCondition从哪来的呢?从debug的情况来看跟WebMvcSecurityConfiguration有关。


看一下WebMvcSecurityConfiguration这个类,它有一个@ConditionalOnMissingBean注解,这个注解作用在bean定义上,它的作用就是在容器加载它作用的bean时,检查容器中是否存在目标类型(ConditionalOnMissingBean注解的value值)的bean了,如果存在这跳过原始bean的BeanDefinition加载动作。  


上面说的那段逻辑就是由OnMissingBeanCondition.matches来完成的。也就是说上面那整棵调用树所做的事情就是检查容器知否已存在RequestDataValueProcessor类型的bean,如果存在则不再重复重新加载这个RequestDataValueProcessor,如果是普通的bean那还好办直接比较bean的类型是否是RequestDataValueProcessor类型就行了,不必去初始化整个bean,但是如果是FactoryBean那就坏事了,因为FactoryBean类型的bean最终创建的bean的类型并不是FactoryBean本身的类型,而是由它的getObject返回值来决定的,所以要拿到bean类型,会调它的getObject方法创建bean之后再比较类型。而Dubbo消费者bean的类型都是com.alibaba.dubbo.config.spring.ReferenceBean,这恰恰是一个FactoryBean,此时会初始化消费者bean,设置group属性时接着初始化引用的的disconfPropertiesReader bean,所以就导致了tragedy。

org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(String, Class<?>)


最后只剩下最后一个问题,WebMvcSecurityConfiguration是从哪来的,搜索代码,直接宣布结果:

由于工程使用了AutoConfiguration特性,框架查找并读取所有名称是*AutoConfiguration的类,所以会找到SecurityAutoConfiguration。

 

这个类定义了@Configuration注解,所以会加载这个类中定义的bean,发现了@Import注解,根据这个标签读取到并解析Spring Boot Web Security Configuration,继续读取到它的内部类,再读取到内部类的内部类,这个内部类上有@Enable Web Mvc Security注解,这个注解又@Import了,这个类有@Enable Web Security注解,注解@Import了 <preltr">Spring Web Mvc 重要选择  </preltr">,这个重要选择会导入org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration:


总结产生问题的最终原因:是因为工程使用了Spring Boot的Auto Configuration功能,而根据这个工程的特点会自动加载安全配置SecurityAutoConfiguration,由此经过一系列的直接或间接的import,会导致框架读取WebMvcSecurityConfiguration,而这个配置在加载RequestDataValueProcessor这个bean定义的时候,由于有@ConditionalOnMissingBean的作用导致框架会检查容器中是否已经存在了RequestDataValueProcessor类型的bean实例,当检查的过程中扫描到sendCouponRemoteApiImpl这个Dubbo消费者bean时,由于它是一个FactoryBean,getObject方法会被调用进而执行了bean的初始化动作,而由于此时由于处理属性占位符的PropertySourcesPlaceholderConfigurer还未被激活,所以导致bean的属性值没有被正确初始化。  


解决方案

治标的方案,考虑到工程主要是提供Dubbo服务无需启用安全配置,可以禁调安全配置的自动加载,@EnableAutoConfiguration注解有exclude属性,通过它可以禁用对SecurityAutoConfiguration的自动加载:


治本的方案,Spring社区也有人提出了相同的问题:https://jira.spring.io/browse/SEC-3063,已确定是Spring的bug,官方已在spring-security-config的4.1.0版本解决了这个问题,删掉了ConditionalOnMissingBean这个注解,看代码WebMvcSecurityConfiguration也不再使用这个注解了,升级spring-security-config到4.1.0解决问题: