当前位置: 首页 > 科技观察

有同事问我:为什么我的Service注入不了?

时间:2023-03-16 21:13:23 科技观察

前言一个同事急忙走过来说:敖丙,帮我看看这个错误,什么情况?我看到一个错误:com.xx.xx.service.impl.XxXServiceImpl中的字段xxxService需要找不到类型为“com.xx.xx.service.XxxService”的bean。我早就知道是怎么回事了,又怕他不知道,就耐心地给她解释。她听后说:是不能写下来,免得下次忘记了。我就是这么没骨气的人,想都没想,就出现了这个错误:这个错误其实是在Spring容器中找不到Bean。出现这个错误时,常见的有两种情况:1.@ComponentScan注解文件中的扫描路径不包含这个类2.如果这个类的头部没有加上@Component注解,那么问题就来了:why@ComponentScan是不是在不加@Component注解的情况下就没有被扫描或者注入到Spring容器中?这个问题有点无厘头(不加@Component注解还想注入到Spring容器中吗?)再问一个问题:为什么@ComponentScan可以扫描出来加@Component注解注入到Spring容器中?当然你可以直接回答:因为Spring有规定当然我反过来问你:MybatisMapper是没有用的@Component注解,为什么可以注入到Spring容器中?笨蛋,你不能回答吗?回答不出来就往下看~问题分析回答:为什么@ComponentScan通过添加@Component注解扫描注入到Spring容器中?我们首先要拆解问题:1、@ComponentScan扫描是干什么的?2、加上@Component注解还代表什么?回答完这两个问题,我们来猜一猜:上面的流程是否可以定制?如何定制?不然就没法解释Mapper是怎么注入到Spring容器中的。@ComponentScan扫描有什么作用?过程大致是这样的:Spring扫描指定包下的类,解析这些类的信息,转换成BeanDefinition,注册到beanDefinitionMap中。那么这个过程的细节是怎样的呢?我们先来了解一下这个过程中涉及的角色:1、BeanDefinition:Bean定义,包含Class的相关信息2、ConfigurationClassPostProcessor:配置类处理器,查找配置类,创建配置类解析器3、ConfigurationClassParser:配置类解析器,解析配置类,创建@ComponentScan注解解析器4、ComponentScanAnnotationParser:@ComponentScan注解解析器,解析@ComponentScan注解,创建Bean定义扫描器5、ClassPathBeanDefinitionScanner:Bean定义扫描器,扫描指定包下的所有类,将匹配的类转换为BeanDefinition6、BeanDefinitionRegistry:BeanDefinition注册器,从上到下注册BeanDefinition,我们不难发现整个过程是有递进关系的:下面我们来看看这些角色的具体职责。1.配置类处理器配置类处理器主要做三件事1.找到配置类2.创建并调用配置类解析器3.加载配置类解析器返回的@Import和@Bean注解类1.1搜索你可能有关于配置类的疑惑。配置类不是我们传入的吗?为什么要找配置类呢?这是因为Spring的整个调用环节非常复杂。不能说是把配置类传给了lower层,但是一开始是在BeanDefinitonMap中注册配置类的。查找配置类大致有两个过程:1、从BeanFactory中获取所有BeanDefiniton信息2、判断BeanDefiniton是否为配置类。第一步很容易解决。所有的BeanDefinitons都放在BeanFactory的BeanDefinitonMap中,直接获取OK。至于第二点,首先我们要知道什么是配置类?在Spring中,有两种配置类:1.full类型:标识@Configuration注解的类2.lite类型:标识@Component@ComponentScan@Import@ImportResource@Bean注解类的唯一区别(有一个就好了)是完整类型的类将在后处理步骤中动态代理。还记得这个例子吗?Q:Spring启动Wheel对象时,New了几次?@ConfiguraitonpublicclassMyConfiguration{@BeanpublicCarcar(){returnnewCar(wheel());}@BeanpublicWheelwheel(){returnnewWheel();}}答案是一次,因为MyConfiguration对象实际上会被cglib动态代理,所以即使被this调用。方法,代理逻辑仍然会被触发。只有在这种情况下才会出现这种情况。通常我们在进行cglib代理的时候,这个调用还是直接调用这个类的方法。找到所有的配置类信息后,接下来就是创建一个配置类解析器,将所有的配置类交给配置类解析器解析1.2流程图二、配置类解析器配置类解析器的职责如下12.分析内部类信息3.分析@PropertySources注解信息4.分析@ComponentScan注解信息5.分析@Import注解信息6.分析@Bean注解信息2.1判断该类是否应该跳过Over-parsing所谓判断一个类是否应该跳过解析,其实就是判断这个类是否打上了@Conditional注解,是否满足条件。如果标识了注解但不满足条件,则跳过解析步骤。比如我们常见的@Profile,@ConditionalOnMissBean等都是通过这个来控制的。2.2分析内部类信息有时候我们的配置类中有内部类,内部类也是配置类,所以需要用到这种方法进行分析。2.3解析@ComponentScan注解信息这一步主要是使用**@ComponentScan注解解析器解析@ComponentScan注解,从而得到BeanDefinition列表,然后判断这些BeanDefinition是否是配置类,然后调用配置类解析器**再次递归解析。流程图3.@ComponentScan注解解析器在这一步中,Spring会将我们配置在@ComponentScan注解上的所有信息提取出来,存储在Bean定义扫描器中,然后使用Bean定义扫描器获取符合条件的BeanDefiniton。excludeFilter和includeFilter用于在扫描时判断类是否满足要求。默认excludeFilter:扫描时排除自己的类默认includeFilter:扫描时判断类是否打@Component注解4.Bean定义扫描器BeanDefinitionScanner主要做了三件事:1.扫描包路径下的类2.BeanDefinition设置值3.使用BeanDefinition寄存器将BeanDefinition注册到容器中4.1扫描包路径下的类扫描包路径的步骤可以简单理解为遍历class文件的过程,遍历包下的各个类,判断是否theclassmeetstherequirementsCondition——标识@Component注解,将满足条件的类转换为BeanDefiniton。此时的BeanDefiniton只有元数据信息,并没有设置具体的值。4.2为BeanDefiniton设置值如果我们在类上添加像这样的注解:@Lazy@Primary@DependsOn,那么我们需要将这些注解转换成实际的属性并在BeanDefiniton中设置。4.3Flowchart5.BeanDefinitionRegistryBeanDefinitionRegistry的作用是把BeanDefinition放到BeanDefinitionMap中,想一想。了解了扫描包的整体流程后,我们再来回顾一下这个问题:MybatisMapper是如何注入到Spring容器中的?像这样这种问题,乍一看很难理解,面试中经常会出现,因为面试官是带着答案问问题的。但是如果我们想一想,我们应该换个角度:Mapper如何在Spring中注册->自定义注解标识的Class如何在Spring中注册?不知道这样问会不会方便一些?方法一、使用TypeFilter我们知道@Component注解是配合默认注册的IncludeFilter使用的,我们也可以使用自定义的IncludeFilter配合我们自定义的注解来使用自定义的Mapper注解@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public@interfaceMapper{}useMapper@MapperpublicclassMyMapper{publicvoidhello(){System.out.println("myMapperhello");}}test添加一个自定义的IncludeFilter进行测试**注意:**this方法只能支持在实体类上标注自定义注解的情况。如果在接口上加上Mapper注解,会出现异常:Nobeannamed'myMapper'available答案很简单,因为接口无法实例化,所以Spring默认会判断如果类不是实体类,它不会被注册到容器中。那么我们如何才能让在Spring中注册了Mapper注解的接口呢?2.自定义Scanner既然Spring的scanner不能支持接口,那我们就重写一下——判断逻辑。开源框架扩展经验:继承整体逻辑,重写一小块逻辑。所以我们的方法很简单:继承ClassPathBeanDefinitionScanner,重写判断Class是否满足的逻辑returnbeanDefinition.getMetadata().isInterface()&&beanDefinition.getMetadata().isIndependent();才允许注册;}//省略构造方法}逻辑变了,现在又有新问题了:如何让Spring使用?通过整体流程,我们知道在解析过程中创建(新建)了Bean定义扫描器**@ComponentScan注解解析器**,我们不能改变这个过程,那么,游戏结束了吗?但是,为什么一定要在Spring的扫描过程中使用我们的scanner呢?Spring的扫描过程结束后再扫描一次不是很好吗?还记得有什么方法可以做到这一点吗?后处理器!3.使用后处理器我们使用BeanDefinitionRegistryPostProcessor在Spring的扫描过程结束后进行后处理。在后处理中,为第二次扫描创建自定义扫描仪。@ComponentpublicclassMapperScannerProcessorimplementsBeanDefinitionRegistryPostProcessor{@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry)throwsBeansException{//创建自定义的扫描器ClassPathMapperScannerclassPathMapperScanner=newClassPathMapperScanner(registry,false);//添加filter,class添加了Mapper注解才注册在Spring中,classPathMapperScanner.addIncludeFilter(newAnnotationTypeFilter(Mapper.class));//在这里你可以从外部设置值而不是写死的classPathMapperScanner.scan("com.my.spring.test.custom");}}使用这种方式,你会发现我们的接口确实注册到了BeanDefinitionMap中。但是,你仍然会收到一个错误:Failedtoinstantiate[com.my.spring.test.custom.InterfaceMapper]:SpecifiedclassisaninterfaceTheinterfacecannotbeinstantiated,althoughweregistereditinSpring.但是Mybatis是怎么做到的呢?答案是更换。Mybatis将图中的beanClass替换为FactoryBean:MapperFactoryBean,然后将原来的beanClass放入其mapperInterface属性中。它的getObject方法看起来像这样publicTgetObject()throwsException{returngetSqlSession().getMapper(this.mapperInterface);}如果你还记得Mybatis最初的用法,你应该对这行代码很熟悉。好了,思考的内容就到这里了。我们只是借用Mybatis的现象来思考,然后再深入到Mybatis的内容中。总结本文分析了开发中的一个常见问题,介绍了Spring的配置类解析扫描过程,同时结合Mybatis中的现象,思考如何让自定义注解标记Class注册到Spring中,并用案例给出更好的答案,希望大家通过案例更深刻的理解过程。同样,通过本次学习,我们来回答以下问题:1、什么是配置类?Spring有哪几种配置类?有什么不同?2、BeanDefinitionRegistryPostProcessor有什么用?你知道任何案例吗?你有心吗?想一想,好人敖丙学会了下课怎么留作业?