前言:
今天咱们对“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源码打不来