性能文章>记一次 SpringBoot 项目启动失败排查 和 DubboReference 源码分析>

记一次 SpringBoot 项目启动失败排查 和 DubboReference 源码分析转载

2年前
451414

问题现象

在我们项目中有一个公司内部的二方包,里面有一个类:MvcInterceptorAutoConfiguration ,里面定一个了一个 Bean accessContextResolver 。生成这个 Bean 需要自动注入另一个 Bean :accessContextService。代码如下:

public class MvcInterceptorAutoConfiguration implements WebMvcConfigurer, ApplicationContextAware {

@Bean
public AccessContextResolver accessContextResolver(@Autowired AccessContextService accessContextService, @Autowired WebAuthConfig webAuthConfig) {
return new DefaultAccessContextResolver(webAuthConfig, accessContextService);
}
}

在我们项目中又有一个类:ProxyCenter ,它里面用 @DubboReference 定义了 accessContextService 。代码如下

@Component
public class ProxyCenter {

@DubboReference(timeout = 10000, check = false, version = "1.0.0")
private AccessContextService accessContextService;

...
}

但是在项目启动过程中报如下的错

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method accessContextResolver in cn.xxx.xxx.xxx.xxx.config.MvcInterceptorAutoConfiguration required a bean of type 'cn.xxx.xxx.xxx.xxx.service.AccessContextService' that could not be found.


Action:

Consider defining a bean of type 'cn.xxx.xxx.xxx.xxx.service.AccessContextService' in your configuration.

这个错误可能大家都很熟悉了,意思是 Spring 在创建 accessContextResolver 这个 Bean 的时候需要自动注入 accessContextService 这个 Bean ,但是 Spring 容器找不到这个 Bean ,所以启动失败。

问题分析

Dubbo版本:2.7.0

分析思路

  • 对于这个问题本质是 @Autowired 不能注入 @DubboReference 声明过的 Bean ,最主要需要弄清楚 @DubboReference 和 @Autowired 所做的事情,并且分别都是在什么时候做的。

  • 如果只使用 @Autowired 的时候,并不会出现以上这种情况,所以我们定位问题的方向优先去看 @DubboReference 的实现逻辑。

@DubboReference实现逻辑分析

背景知识

先讲一个背景知识:我们知道 Spring 创建一个 Bean 大致需要经过实例化对象、属性填充、初始化对象这几步,其中属性填充是在 populateBean 这个方法中实现的(代码如下),这里有一段逻辑是,获取 Bean 工厂中所有的 BeanPostProcessor ,如果是 InstantiationAwareBeanPostProcessor 类型,那么就调用 postProcessPropertyValues 方法。

注意:InstantiationAwareBeanPostProcessor 是一个抽象类,它本身没有提供 postProcessPropertyValues 实现,所有的实现都是在子类中的。

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

...

Iterator var5 = this.getBeanPostProcessors().iterator();

BeanPostProcessor bp = (BeanPostProcessor)var9.next();
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor)bp;
pvs = ibp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}

...
}

以下是 InstantiationAwareBeanPostProcessorAdapter 实现类, 这里只列举了和我们这次问题相关的子类

InstantiationAwareBeanPostProcessorAdapter

AutowiredAnnotationBeanPostProcessor ( Spring 提供属性/方法注入实现)

|

AbstractAnnotationBeanPostProcessor 【com.alibaba.spring......】

|

ReferenceAnnotationBeanPostProcessor 【org.apache.dubbo......】( Dubbo提供的 @DubboReference, @Reference 实现)

从上面的源码和类的继承关系我们可以得出结论:spring进行属性填充的时候,会调用 ReferenceAnnotationBeanPostProcessor 这个类的 postProcessPropertyValues 方法。而 ReferenceAnnotationBeanPostProcessor 这个类就是Dubbo提供的 Bean 的后置处理器, @DubboReference, @Reference 就是在这个方法里面实现的。

源码分析

在了解了上面的背景知识后,我们就开始进入 @DubboReference 的源码分析。下面列出来的是 ReferenceAnnotationBeanPostProcessor 对于 postProcessPropertyValues 的实现。

我们要注意一点,那就是此时正在创建的 Bean 是 proxyCenter,至于为什么是 proxyCenter 这个 Bean ,这个很简单,因为在本案例中 accessContextService 是 ProxyCenter 这个类的属性,所以在创建 proxyCenter 这个 Bean 的时候发生对 accessContextService 这个属性的填充动作。

 public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {

//找对象
InjectionMetadata metadata = this.findInjectionMetadata(beanName, bean.getClass(), pvs);

try {
//执行注入
metadata.inject(bean, beanName, pvs);
return pvs;
} catch (BeanCreationException var7) {
throw var7;
} catch (Throwable var8) {
throw new BeanCreationException(beanName, "Injection of @" + this.getAnnotationType().getSimpleName() + " dependencies is failed", var8);
}
}

postProcessPropertyValues 方法主要做了两件事:

1、找到 @DubboReference 、@Reference 修饰属性,并且将元数据信息封装在 InjectionMetadata 中。

2、执行注入。这里 inject 方法调用的是 AbstractAnnotationBeanPostProcessor 中的inject 方法,也就是执行父类的 inject 方法。

分析思路:既然是在自动注入的时候中没有找到这个对象,也就是说 Spring 容器中没有这个对象,那么有可能是 Dubbo 生成了代理对象,但是没有放到 Spring 容器中,所以自动注入的时候没有找到。所以,我们可以先看 inject 这个方法。(后面也证实了 findInjectionMetadata 并没有什么问题,所以这里就不分析。)

 protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {

Class<?> injectedType = this.resolveInjectedType(bean, this.field);

//生成代理对象
Object injectedObject = AbstractAnnotationBeanPostProcessor.this.getInjectedObject(this.attributes, bean, beanName, injectedType, this);

//反射,给属性设置值
// bean:proxyCenter
// this.field:accessContextService
ReflectionUtils.makeAccessible(this.field);
this.field.set(bean, injectedObject);
}

inject 方法主要是生成代理对象,然后给当前对象的属性设置值。也就是这里会把生成的代理对象 accessContextResolver 设置到当前Bean 也就是 proxyCenter 这个 Bean 的属性上。

分析思路:但是我们的问题不是说这个 Bean 的属性是null,而是在 Spring 自动注入的时候没有拿到对象的值,但是 inject 方法没有涉及到把代理对象 accessContextResolver 放到 Spring 容器中这块代码,所以我们可以继续看往下看。( getInjectedObject 这个方法的核心逻辑是在 doGetInjectedBean ,只是加了缓存操作。所以这里没有列出来)

 protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
InjectionMetadata.InjectedElement injectedElement) throws Exception {

...

ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);

//判断当前这个服务是不是在本地使用@DubboService或者@Service定义出来的
boolean localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);

//如果是本地的service、并且还没有注册过,那么就会触发提前注册服务
prepareReferenceBean(referencedBeanName, referenceBean, localServiceBean);

//将服务信息 放到bean工厂里面,还没有涉及到获取真正的服务
//1、本地暴露出去的服务
//2、需要从注册中心读取的服务
registerReferenceBean(referencedBeanName, referenceBean, attributes, localServiceBean, injectedType);

//拿到远端的服务
return referenceBean.get();
}

doGetInjectedBean 这个方法是 @DubboReference 实现的核心,这里每一步都写了注释。

分析思路:这里有个方法 registerReferenceBean ,顾名思义,应该是注册 ReferenceBean ,这里注册应该是把当前 Bean 注册到 Bean 工厂里面,那么我们需要的答案应该就在这个方法里面。( ReferenceBean 是一个对象,封装了 applicationContext、接口的代理对象:ref 等等,其中 ref 就是生成的代理对象,比如 @DubboReference AService aService ; 那么 ref 就是 aService 的代理对象。这个对象会被包装成一个 ReferenceBean, 所以可以粗暴的认为 ReferenceBean 就是一个 服务具体的引用者。)

private void registerReferenceBean(String referencedBeanName, ReferenceBean referenceBean,
AnnotationAttributes attributes,
boolean localServiceBean, Class<?> interfaceClass) {

ConfigurableListableBeanFactory beanFactory = getBeanFactory();

String beanName = getReferenceBeanName(attributes, interfaceClass);

//情况一:@Service 是本地的
if (localServiceBean) {

//如果是本地的话,服务的所有信息都在本地,
AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) beanFactory.getBeanDefinition(referencedBeanName);
RuntimeBeanReference runtimeBeanReference = (RuntimeBeanReference) beanDefinition.getPropertyValues().get("ref");
// @Service 对应的bean名称
String serviceBeanName = runtimeBeanReference.getBeanName();

// 没有新创建一个bean 而是沿用@Service生成的bean,这样就避免bean重复
beanFactory.registerAlias(serviceBeanName, beanName);

} else {

//@Service是远端的
if (!beanFactory.containsBean(beanName)) {

//spring动态注册bean
beanFactory.registerSingleton(beanName, referenceBean);
}
}
}

registerReferenceBean 这个方法主要是把 ReferenceBean 注册到 Spring 中。至此,我们也找到了我们的答案,那就是: @DubboReference 会把生成的代理对象放到 Spring 容器中,而且触发的时机是在创建 @DubboReference 修饰属性对应的这个 Bean 创建的过程中。也就是说只要那个 Bean 没有被创建,那么 @DubboReference 修饰的属性是不会放到 Spring 容器中的。

上面的步骤用流程图来表示就是:

 

总结

上面就是 @DubboReference 在 Spring 启动过程中触发的时机,也就是说在 Spring 创建 Bean 的时候,在属性填充阶段,如果发现@DubboReference 修饰的属性,ReferenceAnnotationBeanPostProcessor 这个 Bean后置处理器会创建这个服务引用的代理对象,然后放到 Spring 容器中。

分析思路:所以文章开头的问题其实就可以理解为:Spring 在创建 proxyCenter 这个 Bean 的时候就会实例化 accessContextService 对象,然后放到 Spring 容器中,但是在使用 @Autowired 进行对 accessContextService 注入的时候,却没有找到这个 Bean 。这时候极有可能的原因:就是 Spring 先使用 @Autowired 进行对 accessContextService 注入,然后才会发生创建 proxyCenter 这个 Bean 。

我们知道 @Autowired 自动注入的时候,如果 Bean 不存在,那么就会触发创建 Bean 的过程,下面我们分析下 @Autowired 实现逻辑,为什么这里的对象是 null 。

@Autowired实现逻辑分析

说明:由于@Autowired实现逻辑比较复杂,下面列出的代码都是和本案例相关的代码,其他代码会做相应省略。

分析思路

  • @Autowired 这个注解可以修饰属性、方法、入参等,@Autowired 作用的对象不同处理的时机也不同,比如 @Autowired 修饰属性或者方法的时候,就是在属性填充的时候处理的,而本文案例中对于 @Autowired 处理是在实例化 Bean 的时候。

@Bean
public AccessContextResolver accessContextResolver(@Autowired AccessContextService accessContextService, @Autowired WebAuthConfig webAuthConfig) {
return new DefaultAccessContextResolver(webAuthConfig, accessContextService);
}

源码分析

在 Bean 的实例化过程中,有一个步骤是:createArgumentArray,这里有一种情况是创建自动注入参数:ConstructorResolver#resolveAutowiredArgument ,这个就是本案例分析的入口,由于下面很多逻辑和本案例无关,这部分代码就不列举出来了,大家可以自行查看。我们这边从 doResolveDependency 这个方法开始看起。

注意一点:beanName 是指当前要创建的 Bean 名称,而不是自动注入的 Bean 名称。本案例中指的是 accessContextResolver 而不是 accessContextService 。可以看上面的代码。

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

//31处理普通bean key:自动注入的bean名称 ;value:class对象 或者是具体的bean
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
//如果根据bean名称没有获取到bean,@Autowire(required=true) 这种情况的话,那么就报异常
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
//如果是@Autowire(required=false)那么直接返回null
return null;
}

String autowiredBeanName; //自动注入的bean的名字
Object instanceCandidate; //自动注入的对象

//如果根据bean名称,找到了不只一个对象
if (matchingBeans.size() > 1) {
// @Primary -> @Priority -> 方法名称或字段名称匹配
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(type, matchingBeans);
}
else {
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
else {
// 根据type,只找到了一个bean信息,那么这个就是我们要的对象
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}

if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}

//这里用来判断 返回的是已经创建好的bean 还是 只是class ,如果是class 那么需要执行创建bean的逻辑,获取到真的bean对象
//因为注入的时候需要的是个对象,class没有用
if (instanceCandidate instanceof Class) {
//这里其实是执行getBean()逻辑
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
}
Object result = instanceCandidate;
if (result instanceof NullBean) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
result = null;
}
if (!ClassUtils.isAssignableValue(type, result)) {
throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
}
return result;
}
}

doResolveDependency 这个方法是依赖注入的核心方法,里面一共做了以下几件事情:

1、处理 @Value 修饰的参数

2、处理 MultipleBean,也就是 List、Map、Array、Set 修饰的对象。比如:@Autowire private List  aServiceList ; 这种情况。

3、处理普通注入

31、首先:根据类型,查找所有的bean名称,放到map中。findAutowireCandidates返回一个map ,map 的 key:Bean名称,value:有可能是已经创建的 Bean,有可能是bean还没有创建,返回的是class对象。

32、matchingBeans 的 size > 1 :也就是说同一个 type,找到多个 Bean。一种是同一个类生成多个对象,比如多数数据源,还有就是一个接口多个实现,在注入的时候只注入接口。这时候会根据优先级取第一个(@Primary -> @Priority )

33、matchingBeans 的 size =1 :这个就是我们需要的对象。

34、判断这个对象是否是class的实例,如果是,然后进行创建 Bean 过程

4、最后返回这个对象。

分析思路:步骤【31】这里会有个问题,如果根据类型找不到 Bean 信息,那么如果这个还是 @Autowire(require=true) 这种情况,那么就会执行 raiseNoMatchingBeanFound 这个方法会报一些异常。个人猜想,我们这边启动报错会不会就是这边根据类型 AccessContextService 没有找到对应的 Bean 信息,所以才会报错?我们接着往下看 findAutowireCandidates 这个实现逻辑。(虽然下面也有一些场景会报错,但是和这个案例情况并不符合)

protected Map<String, Object> findAutowireCandidates(
@Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {

//根据requiredType找到所有这个type的bean名称
String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this, requiredType, true, descriptor.isEager());

Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);

//根据bean名称查找对应的bean或者是class对象,放到map里面,注意:这里如果这个bean还没有实例化,不会提前实例化
...

return result;
}

findAutowireCandidates 方法是根据类型,找到找所有的 Bean 名称和对象的过程。

分析思路:这里的两部都可能出现问题:

1、查找所有的 Bean 名称的时候 Bean 名称没有找到;

2、根据名称查找对应的 Bean 或者是 class 对象;

优先考虑第一步是不是根据类型查找 Bean 名称没有找到。因为我们刚刚分析 @DubboReference 的时候,有一段代码:是动态注册 Bean ,注册的过程中会把这个 Bean 名称放到 manualSingletonNames 对象中。但是这个放进去的时机是在创建 proxyCenter 的时候。

beanFactory.registerSingleton(beanName, referenceBean);


//registerSingleton 实现逻辑
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {

...

if (!this.beanDefinitionMap.containsKey(beanName)) {
this.manualSingletonNames.add(beanName);

}

下面我们着重看 Spring 是怎么找到所有的 Bean 名称的,这个主要逻辑是在 doGetBeanNamesForType 方法中。

// includeNonSingletons:是否包含非单利的
//allowEagerInit :处理factoryBean的
private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
List<String> result = new ArrayList<>();

// 循环bean工厂所有的bean的定义
for (String beanName : this.beanDefinitionNames) {

if (!isAlias(beanName)) {
try {

RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);

//bean是非抽象的
if (!mbd.isAbstract() &&
//
(allowEagerInit ||(mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading()) &&
//factoryBean相关处理
!requiresEagerInitForType(mbd.getFactoryBeanName()))) {

//判断是不是factoryBean
boolean isFactoryBean = isFactoryBean(beanName, mbd);
BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();

//条件一:非factoryBean、存在dbd并且是非懒加载 或者是单利池里面已经有这个bean了
//条件二:包含非单利的,或者是这个bean是单利的
//条件三:type和 当前bean的type类型一致
boolean matchFound =
(allowEagerInit || !isFactoryBean ||(dbd != null && !mbd.isLazyInit()) || containsSingleton(beanName)) &&
(includeNonSingletons ||(dbd != null ? mbd.isSingleton() : isSingleton(beanName))) &&
isTypeMatch(beanName, type);

//处理factoryBean
if (!matchFound && isFactoryBean) {
beanName = FACTORY_BEAN_PREFIX + beanName;
matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
}

//匹配上了,就放到集合里面,后面返回
if (matchFound) {
result.add(beanName);
}
}
}
catch (CannotLoadBeanClassException ex) {
//异常处理
onSuppressedException(ex);
}
catch (BeanDefinitionStoreException ex) {
//异常处理
onSuppressedException(ex);
}
}
}


// 处理手动注册的bean registerSingleton(beanName,Object)
//spring 除了扫描一些注解例如@Service、@Compoment 还可以在代码中手动注册
for (String beanName : this.manualSingletonNames) {
....
}

return StringUtils.toStringArray(result);
}

doGetBeanNamesForType 根据 Bean 的类型查找所有符合 Bean 名称。注意:这里包含了普通 Bean 和 FactoryBean ,同时,这里匹配的 Bean 包括自动注册和手动注册的。我们可以看到,这里循环了两个对象:beanDefinitionNames 和 manualSingletonNames

beanDefinitionNames 这个对象里面的 Bean 信息是 Spring 在初始化的时候扫描了项目中的类似于 @Compoment、@Service 等注解生成的,我们的 AccessContextService 肯定不会在这个对象里面,因为 AccessContextService 并不是按照 Spring 定义的 Bean 规范定义的 Bean,manualSingletonNames 是registerSingleton 调用的时候放进去的。

总结

至此,问题的原因已经很清楚了:Spring 在启动过程中,先进行 @Autowired 处理,这时候主要注入 AccessContextService 这个类型的 Bean,但是他不是我们使用 @Service 、@Compoment 等 Spring提供的定义 Bean 的方式定义的 Bean,所以 Spring 容器中不会有 AccessContextService 任何 Bean 的定义信息,而这时候 proxyCenter 这个对象还没有实例化,没有发生属性填充, AccessContextService 这个类的代理对象就没有注入到 Spring 环境中,所以就无法获取 AccessContextService 类型对象,Spring 启动报错。

解决思路

1、本案例的核心问题是:Bean 的使用优先于 Bean 的创建,但这个 Bean 又不是按照 Spring 规范定义的 Bean,所以没有办法在自动注入找不到的时候自己创建。所以我们只要保证先创建 Bean ,后注入 Bean 就可以了。

基于这样的话,解决方法可以是:让 proxyCenter 这个 Bean 先于 accessContextResolver 实例化,因为在创建的时候会对属性进行填充,这时候就会触发 AccessContextService 这个远程服务的实例化,但是 Spring Bean 的创建是无序的,怎么让这两个 Bean 按照一定顺序创建呢?

Spring 中可以使用 @DependsOn 这个注解让某个 Bean 优先于另一个 Bean 被创建,但是在我们这个案例中,accessContextResolver 处于二方包中,加 @DependsOn 并不现实。所以我们可以定义一个 BeanFactoryPostProcessor ,然后手动修改 accessContextResolver 对应的 BeanDefinition,这样就解决问题啦。代码如下:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("accessContextResolver");
beanDefinition.setDependsOn("proxyCenter");
}
}

个人思考

以上解决方式并不是很优雅,Dubbo使用 Bean 后置处理器实现 @DubboReference 这种实现方式存在缺陷,@DubboReference 并不是 Spring定义的 Bean,所以不会生成 BeanDefinition ,也就是不会主动 createBean ,只能在属性注入的时候触发,这就会导致本文这种问题。我觉得比较好的实现方式 应该是在 Spring 没有实例化任何 Bean 之前,把所有 @DubboReference 对应的对象都事先创建出来,然后在 Spring 创建 Bean 的时候,拿来即用,那么就不会出现以上的问题。

上面的问题Dubbo在后续版本(3.0.0)中已经解决了,所以我们之前的问题也可以使用升级 Dubbo 版本来解决。至于 Dubbo 后面是怎么解决这个问题的,这里不具体展开讲修改后的实现逻辑,大家有兴起可以自行翻看源码。

 

作者:政采云技术团队

文章来源:微信公众号

原文链接:https://mp.weixin.qq.com/s/E6mANd7Glr--06lw96hieA

点赞收藏
分类:
嘿小黑
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

Redis stream 用做消息队列完美吗?

Redis stream 用做消息队列完美吗?

Netty源码解析:writeAndFlush

Netty源码解析:writeAndFlush

4
1