龙空技术网

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

编程菌zfn 129

前言:

今天咱们对“apache源码打不来”可能比较着重,你们都需要剖析一些“apache源码打不来”的相关资讯。那么小编同时在网摘上收集了一些关于“apache源码打不来””的相关知识,希望你们能喜欢,看官们快快来学习一下吧!

问题现象

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

@Componentpublic 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,这样就解决问题啦。代码如下:

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

作者:政采云技术团队

链接:

标签: #apache源码打不来