丨spring bean依赖注入常见错误上_第1页
丨spring bean依赖注入常见错误上_第2页
丨spring bean依赖注入常见错误上_第3页
丨spring bean依赖注入常见错误上_第4页
丨spring bean依赖注入常见错误上_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

requiredasinglebean,but2were顾名思义,我们仅需要一个Bean,但实际却提供了2个(这里的“2”在实际错误中可能是其它大于1的任何数字)。为了重现这个错误,我们可以先写一个案例来模拟下。假设我们在开发一个学籍管理系统案例,需要提供一个API根据学的学号ID)移除,学信息肯定一个数据库来支撑,所以大体上可以实现如下:代代123456789publicclass{DataService(path="students/{id}",method=publicvoiddeleteStudent(@PathVariable("id")@Range(min=1,max=100)in}其中DataService是一个接口,其实现依托于Oracle,代码示意如代代123456789publicinterface{voiddeleteStudent(intpublicclassOracleDataServiceimplementsDataService{@OverridepublicvoiddeleteStudent(intid)("deletestudentinfomaintainedby}}非的业务从Oracle迁移到社区版Casandra,所以我们自然会先添加上一个新的DataService实现,代码如下代代publicclassCassandraDataServiceimplementspublicvoiddeleteStudent(intid)("deletestudentinfomaintainedby 8实际上,当我们完成支持多个数据库的准备工作时,程序就已经无法启动了,报案例解要找到这个问题的根源,我们就需@Autowired现的依赖注入的原理有一定的了解。首先,我们先来了解下@Autowired发生的位置和过程。当一个Bean被构建时,包括两个基本步骤 AutowireCapableBeanFactory#createBeanInstance方法:通过构造器反射构造出这个Bean,在此案例中相当于构建出StudentController的实例; AutowireCapableBeanFactory#populate方法:填充(即设置)这个Bean,在本案例中,相当于设置StudentController实例中被@Autowired标记的dataService属性成员。在步骤2,“填充”过程的关键就是执行BeanPostProcessor理器,关键代码1123456789代protectedvoidpopulateBean(StringbeanName,RootBeanDefinitionmbd,//省略非关键代for(BeanPostProcessorbp:getBeanPostProcessors())(bpinstanceofInstantiationAwareBeanPostProcessor){InstantiationAwareBeanPostProcessoribp=PropertyValuespvsToUse=ibp.postProcessProperties(pvs,//省略非关键代}}}}在上述代码执行过程中,因为StudentController含有标记为Autowired的成员属dataService,所以会使用到(BeanPostProcessor中的一种)来完成“装配”过程:找出合适的DataService的bean并设置给StudentController#dataService。如果深究这个装配过程,又可以细AutowiredAnnotationBeanPostProcessor#postProcessProperties中的代码代代1InjectionMetadatametadata=findAutowiringMetadata(beanName,AutowiredFieldElement#inject方法代代protectedvoidinject(Objectbean,@NullableStringbeanName,@NullableFieldfield=(Field)Object//省略非关键代tryDependencyDescriptordesc=newDependencyDescriptor(field,889//寻找“依赖”,desc为"dataService"的value=beanFactory.resolveDependency(desc,beanName,}}if(value!={;field.set(bean,value);}说到这里,我们基本了解了@Autowired过程发生的位置和过程。而且很明显,我们案例中的错误就发生在上述“寻找依赖”的过程中(上述代码的第9行),那么到底是怎么发为了更清晰地展示错误发生的位置,我们可以采用调试的视角展示其位置(DefaultListableBeanFactory#doResolveDependency中代码片段),参考下图如上图所示,当我们根据DataService这个类型来找出依赖时,我们会找出2个依赖,分别为CassandraDataService和OracleDataService。在这样的情况下,如果同时满足以调用determineAutowireCandidate来选出优先级最高的依赖,但是发现并没有代代protectedStringdetermineAutowireCandidate(Map<String,Object>candidates,Class<?>requiredType=StringprimaryCandidate=determinePrimaryCandidate(candidates,4556789if(primaryCandidate!={return}StringpriorityCandidate=determineHighestPriorityCandidate(candidates,reif(priorityCandidate!=null){return}//for(Map.Entry<String,Object>entry:{StringcandidateName=entry.getKey();ObjectbeanInstance=entry.getValue();if((beanInstance!=null&&this.resolvableDependencies.containsValue(bmatchesBeanName(candidateName,descriptor.getDependencyName())){return}}return如代码所示,优先级的决策是先根据@Primary来决策,其次是@Priority决策,最后是根据Bean名字的严格匹配来决策。如果这些帮助决策优先级的注解都没有被使用,名字也不精确匹配,则返回null,告知无法决策出哪种最合适。@Autowired求是必须注入的(即required默认值为true),或者注解的属性类型并不是可以接受多个Bean的类型,例如数组、Map、集合。这点可以参考DefaultListableBeanFactory#indicatesMultipleBeans的实现:代代privatebooleanindicatesMultipleBeans(Class<?>type)return(type.isArray()||(type.isInterface()(Collection.class.isAssignableFrom(type)||4对比上述两个条件和我们的案例,很明显,案例程序能满足这些条件,所以报错并不奇问题修针对这个案例,有了源码的剖析,我们可以很快找到解决问题的方法:打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择。不过需要你注意的是,不是每一种条件的打破都满足实际需求,例如我们可以通过使用标记y让被标记的候选者有更高优先级,从而避免报错,但是它并不一定符合业务需求,这就好比我们本身需要两种数据库都能使用,而不是顾此失彼。代代publicclassOracleDataServiceimplements//省略非关键代6现在,请你仔细研读上述的两个条件,要同时支持多种DataService,且能在不同业务情景下精确匹配到要选择到的DataService,我们可以使用下面的方式去修改:代代DataService如代码所示,修改方式的精髓在于将属性名和Bean名字精确匹配,这样就可以让注入选择不:需要Oracle时指定属性名为oracleDataService,需要Cassandra时则指定属性名为cassandraDataService。案例2:显式Bean时首字母忽略大小针对案例1的问题修正,实际上还存在另外一种常用的解决办法,即采用@Qualifier来显式指定的是那种服务,例如采用下面的方式:代代DataService这种方式之所以能解决问题,在于它能让寻找出的Bean有一个(即精确匹配),所以9raiseNoMatchingBeanFound(type,}return}//省略其他非关键代if(matchingBeans.size()>1)//省略多个bean的决策过程,即案例1重点介绍内}//省略其他非关键代18我们会使用@Qualifier指定的名称去匹配,最终只找到了唯一一个不过在使用@Qualifier时,我们有时候会犯另一个经典的小错误,就是我们可能会Bean的名称首字母大小写。这里我们把校正后的案例稍稍变形如下代代DataService运行程序,我们会报错如Exceptionencounteredduringcontextinitialization-cancellingrefreshattempt:org.springframework.beans.factory.UnsatisfiedDependencyException:Errorcreatingbeanwithname'studentController':Unsatisfieddependencyexpressedthroughfield'dataService';nestedexceptionisorg.springframework.beans.factory.NoSuchBeanDefinitionException:Noqualifyingbeanoftype'com.spring.puzzle.class2.example2.DataService'available:expectedatleast1beanwhichqualifiesasautowirecandidate.Dependencyannotations:这里我们很容易得出一个结论:对于Bean名字,如果没有显式指明,就应该是类名,不妨再测试下,假设我们需要支 这种数据库,我们定义了一个命名DataService的实现,然后借鉴之前的经验,我们很容易使用下面的代码来这代代123DataService 则运行通过了。这和之前的结论又了。所以,显式Bean时,首字母到底是大写案例解对于这种错误的报错位置,其实我们正好在本案例的开头就贴出了(码第9代代1raiseNoMatchingBeanFound(type,descriptor.getResolvableType(),即当因为名称问题(例如Bean首字母搞错了)找不到Bean时,会直接抛NoSuchBeanDefinitionException在这里,我们真正需要关心的问题是:不显式设置名Bean,其默认名称首字母到底是看案例的话,当我们启动基于SpringBoot的应用程序时,会自动扫描我们的Package,以找出直接或间接标记了@Component的Bean的定义(即BeanDefinition)。例如CassandraDataService、DataService都被标记了@Repository,而Repository本身被@Component标记,所以它们都是间接标记了@Component一旦找出这些Bean的信息,就可以生成这些Bean的名字,然后组合成一BeanDefinitionHolder返回给上层。这个过程关键步骤可以查看下图的代码片BeanNameGenerator#generateBeanName即用来产生Bean的名字,它有两种实现方式。因为DataService的实现都是使用注解标记的,所以Bean名称的生成逻辑最终调用的其实是AnnotationBeanNameGenerator#generateBeanName这种实现方式,我们3(definitioninstanceofAnnotatedBeanDefinition)4StringbeanName=5if(StringUtils.hasText(beanName))6//Explicitbeanname7return8}9}//Fallback:generateauniquedefaultbeanreturnbuildDefaultBeanName(definition,}大体流程只有两步:看Bean有没有显式指明名称,如果有则用显式名称,如果没有则产生一个默认名称。很明显,在我们的案例中,是没有给Bean指定名字的,所以产生的Bean称就是生成的默认名称,查看默认名的产生buildDefaultBeanName,其代代protectedStringbuildDefaultBeanName(BeanDefinitiondefinition)StringbeanClassName=Assert.state(beanClassName!=null,"NobeanclassnameStringshortClassName=return6首先,获取一个简短的ClassName,然后调用Introspector#decapitalize法,设置首1staticStringdecapitalize(Stringname)2(name==null||name.length()==0)3return4}5(name.length()>1&&Character.isUpperCase(name.charAt(1))67return charchars[]=chars[0]=returnnew12DataService的Bean,其名称应该就是类名本身,而CassandraDataService的Bean名称则变成了首字母小写(cassandraDataService)问题修现在我们已经从源码级别了解了Bean名字产生的规则,就可以很轻松地例中的两个错误了。以CassandraDataService类型的Bean的错误修正为例,可以采用下面处纠正首字母大小写代代DataService定义处显式指定Bean名字,我们可以保持代码不变,而通过显式指CassandraDataServiceBean称为CassandraDataService纠正这个问代代publicclassCassandraDataServiceimplementsDataService//省略实5现在,我们的程序就可以精确匹配到要找的Bn案例3:内部类的Bean遗忘类解决完案例,是不是就意味着我们能搞定所有Bn的显式,不再犯错了呢?天真atarvic,代码如下:代代publicclassStudentControllerpublicstaticclassInnerClassDataServiceimplements4456789publicvoiddeleteStudent(intid)//}}//}遇到这种情况,我们一般都会很自然地用下面的方式直接去显式这 代代DataService很明显,有了案例2的经验,我们上来就直接采用了首字母小写以避免案例2中的错误,但这样的代码是不是就没问题了呢?实际上,仍然会报错“找不 Bean”,这是为什么案例解实际上,我们遭遇的情况是如何内部类的B。解析案例2的时候我曾经贴出了如何产生默认BnAnnotatiBameGenerator#bueftBeass名字的处理,代码如下:StringshortClassName=我们可以看下它的实现,参考ClassUtils#getShortName方法代代123456789publicstaticStringgetShortName(StringclassName){Assert.hasLength(className,"Classnamemustnotbeempty");intlastDotIndex=className.lastIndexOf(PACKAGE_SEPARATOR);intnameEndIndex=className.indexOf(CGLIB_CLASS_SEPARATOR);if(nameEndIndex==-1){nameEndIndex=}StringshortName=className.substring(lastDotIndex+1,shortName=ce(INNER_CLASS_SEPARATOR,return}很明显,假设我们是一个内部类,例如下面的在经过这个方法的处理后,我们得到的其实是下面这个最后经过Introspector.decapitalize的首字母变换,最终获取的Bean名称如所以我们在案例程序中,直接使 innerClassDataService自然找不到想要的Bean问题修通过案例解析,我们很快就找到了这个内部类 的问题顺手就修正了,如下代代DataServic

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论