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

问题现象

在我们项目中有一个公司内部的二方包,里面有一个类: 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 容器中的。

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

img

总结

上面就是 @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 后面是怎么解决这个问题的,这里不具体展开讲修改后的实现逻辑,大家有兴起可以自行翻看源码。