牛逼哄哄的Spring是怎么被MyBatis给征服了
前言其实前几篇文章已经写了好多有关于Spring源码的文章 , 事实上 , 很多同学虽然一直在跟着阅读、学习这些Spring的源码教程 , 但是一直都很迷茫 , 这些Spring的源码学习 , 似乎只是为了面试吹逼用 , 我大概问过一些同学 , 很多同学看了很长时间的Spring但是依旧不知道如何将这些学到的知识运用到实际的案例上!其实这个问题很好解决 , 如果你在开发中很少能够遇见需要Spring扩展时 , 不妨把目光放到一些依托于Spring的项目 , 看看它们是如何运用Spring的扩展点的 。 对于Spring的学习 , 我认为最终真正学会的一定是在某一天 , Spring本身功能不够 , 其他框架解决不了 , 你能够使用自身所学 , 扩展Spring的实现 , 从而完成一些特定的功能 , 我愿称之为牛逼!
一、你一定用到过的MyBatis-Spring我个人而言 , 是十分喜欢MyBatis的开发者的 , 为什么呢?不光是因为他的功能强大 , 更多的是因为其开发团队的良心!为什么这么说呢?感兴趣的小伙伴可以进入的MyBatis-Spring的源码中 , 你会发现一件事 , MyBatis-Spring并不是由Spring进行开发的 , 而是MyBatis自己进行开发的!为什么呢?看一下官方的说法:
Spring2.0只支持iBatis2.0 。 那么 , 我们就想将MyBatis3的支持添加到Spring3.0中(参见SpringJira中的问题) 。 不幸的是 , Spring3.0的开发在MyBatis3.0官方发布前就结束了 。 由于Spring开发团队不想发布一个基于未发布版的MyBatis的整合支持 , 如果要获得Spring官方的支持 , 只能等待下一次的发布了 。 基于在Spring中对MyBatis提供支持的兴趣 , MyBatis社区认为 , 应该开始召集有兴趣参与其中的贡献者们 , 将对Spring的集成作为MyBatis的一个社区子项目 。
于是乎 , MyBatis自己动手搞了一个Spring的扩展实现 , 呕吼!牛逼!
众所周知 , MyBatis作为一个持久层框架它支持自定义SQL、存储过程以及高级映射 。 通过xml映射到接口 , 使开发者使用接口的方式就能够轻松的映射、解析、执行xml中的sql!
但是 , 你想没想过一件事 , MyBatis和Spring整合之后,里面的接口居然能够被Spring进行管理 , 然后通过自动注入等Spring的注入手段进行注入!有的同学可能没听明白 , 翻译过来就是 , Spring原本只能够管理一个普通类 , 但是MyBatis只有一个接口 , 并没有实现类 , Spring是如何进行管理的呢?
二、MyBatis如何对Spring进行扩展1.术语介绍ImportBeanDefinitionRegistrar:这个类是干嘛的?简单来说 , 他可以创建一个自定义的BeanDefinition然后手动的注册到Spring容器中去 。 BeanDefinitionRegistryPostProcessor:他是Spring生命周期中一个重要的环节 , 阅读过之前文章的同学应该记得 , Spring生命周期中 , 会将Class解析成BeanDefinition然后注册在BeanFactory中,然后在执行BeanFactoryPostProcessor之前执行这个类的回调 , 完整一些特定的功能 , 比如注册一波自定义的bd等操作!ClassPathBeanDefinitionScanner:他是Spring内置的一个扫描器 , 可以扫描底层的class文件 , 从而最终完成从class文件到BeanDefinition的转换!2.源码解析使用过SpringBoot的同学都知道 , 如果想要MyBatis使用Spring的自动配置功能 , 都需要在启动类上加上一个@MapperScan,他也是今天的一个源码的重点!
我们先看一下注解@MapperScan究竟做了哪些事情!
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented//这个是一个重点 , 这个注解向Spring中导入了一个MapperScannerRegistrar类//他是ImportBeanDefinitionRegistrar的子类@Import(MapperScannerRegistrar.class)@Repeatable(MapperScans.class)public@interfaceMapperScan{.....忽略不必要代码.....String[]basePackages()default{};.....忽略不必要代码.....}这个注解通过@Import向Spring注入了一个MapperScannerRegistrar,我们进入到他里面看一下源码!
publicclassMapperScannerRegistrarimplementsImportBeanDefinitionRegistrar,ResourceLoaderAware{.....忽略不必要代码...../***Spring回调的时候会回调这个方法*@paramimportingClassMetadata导入类的原信息*@paramregistry注册工具*/@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){//获取对应类MapperScan注解的全部属性信息AnnotationAttributesmapperScanAttrs=AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));if(mapperScanAttrs!=null){//调用具体的实现registerBeanDefinitions(importingClassMetadata,mapperScanAttrs,registry,generateBaseBeanName(importingClassMetadata,0));}}/***注册一个BeanDefinition , 这里会构建并且向容器中注册一个bd也就是一个自定义的扫描器MapperScannerConfigurer*@paramannoMeta被@Importd的类的原信息*@paramannoAttrs注解的元信息 , 内部包含所有的注解属性*@paramregistrySpring提供的注册到容器的工具类*@parambeanNamebean的名称*/voidregisterBeanDefinitions(AnnotationMetadataannoMeta,AnnotationAttributesannoAttrs,BeanDefinitionRegistryregistry,StringbeanName){//构建一个BeanDefinition他的实例对象是MapperScannerConfigurer//他实际上是一个BeanDefinitionRegistryPostProcessor对象未来通过Spring对这个类进行创建和回调BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);.....忽略不必要代码.....//向这个bd里面注入一个basePackage属性 , 未来可以通过属性注入的方式注入到MapperScannerConfigurer的属性中builder.addPropertyValue("basePackage",StringUtils.collectionToCommaDelimitedString(basePackages));registry.registerBeanDefinition(beanName,builder.getBeanDefinition());}.....忽略不必要代码.....}这一段代码最终的逻辑简单来说就是构建了一个自定义扫描器MapperScannerConfigurer然后注册到Bean工厂中 , 他也就是前面术语项中说的BeanDefinitionRegistryPostProcessor的实现类 , Spring声明周期中 , 会自动回调postProcessBeanDefinitionRegistry()方法 , 进行一系列的操作 。 我们下一步就是进入到MapperScannerConfigurer中看一下他做了哪些操作!
publicclassMapperScannerConfigurerimplementsBeanDefinitionRegistryPostProcessor,InitializingBean,ApplicationContextAware,BeanNameAware{/***自定义扫描器*@paramregistry注册到bean工厂的工具类*/@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry){if(this.processPropertyPlaceHolders){processPropertyPlaceHolders();}//构建一个自定义的扫描器他是ClassPathBeanDefinitionScanner的子类//可以扫描项目下的class文件转换成BeanDefinitionClassPathMapperScannerscanner=newClassPathMapperScanner(registry);.....忽略不必要代码.....//这一步是很重要的 , 他是注册了一系列的过滤器 , 使得Spring在扫描到Mapper接口的时候不被过滤掉scanner.registerFilters();//开始执行扫描程序传入对应要扫描的包路径scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage,ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));}}这一段代码主要是在Spring回调这个方法后 , 这个方法会构建一个ClassPathMapperScanner扫描器 , 他是前面术语项中说到的ClassPathBeanDefinitionScanner的子类实现 , 然后调用ClassPathMapperScanner的scan方法 , 将扫描到的类转换成对应的BeanDefinition注册到容器中 , 正常来说我们应该关注的是scan方法 , 但是但是 , 我们在看scan之前 , 应该重点的关注一下registerFilters方法 , 我们大可看一下他做了哪些操作!然后再去看scan方法!
/***配置父扫描程序以搜索正确的界面 。 它可以搜索所有接口或仅搜索那些*扩展了markerInterface或/和那些用notificationClass注释的标记*/publicvoidregisterFilters(){booleanacceptAllInterfaces=true;//如果指定指定注解标注的Mapperif(this.annotationClass!=null){addIncludeFilter(newAnnotationTypeFilter(this.annotationClass));acceptAllInterfaces=false;}//指定接口的Mapper接口if(this.markerInterface!=null){addIncludeFilter(newAssignableTypeFilter(this.markerInterface){@OverrideprotectedbooleanmatchClassName(StringclassName){returnfalse;}});acceptAllInterfaces=false;}//默认的添加所有的Mapper接口为MyBatis类if(acceptAllInterfaces){//默认包含所有类的过滤器addIncludeFilter((metadataReader,metadataReaderFactory)->true);}//排除package-info.javaaddExcludeFilter((metadataReader,metadataReaderFactory)->{StringclassName=metadataReader.getClassMetadata().getClassName();returnclassName.endsWith("package-info");});}为什么要先看这个呢?因为对于Spring而言 , 他对一个BeanDefinition有着很严格的校验 , 当扫描的类不符合预定的一些条件的时候 , Spring就会把它丢弃掉 , 不会管理这个类 , 我们这个方法就是为了 , 让Spring在扫描到那些接口的时候 , 添加一些自定义的过滤器 , 使Spring能够识别我们预定的这些接口 , 然后转换成BeanDefinition!
自定义的过滤器添加完毕后 , 我们就进入到scan方法去!
/***在指定的基本程序包中执行扫描 。 *@parambasePackages包以检查带注释的类*@return注册的bean的数量*/publicintscan(String...basePackages){//获取现有的总数bdintbeanCountAtScanStart=this.registry.getBeanDefinitionCount();//开始扫描逻辑doScan(basePackages);.....忽略不必要代码.....//统计本次扫描新增加的BeanDefinition数量使用总共的数量-原本的数量return(this.registry.getBeanDefinitionCount()-beanCountAtScanStart);}这一步没的说 , 他会统计一下本次新加的一个bd的数量 , 我们进入到scan方法
/***调用父级搜索 , 该搜索将搜索并注册所有候选者 。 然后注册的对象处理以将它们设置为MapperFactoryBeans*@parambasePackages要扫描的包路径*@return对应的BeanDefinition的包装类*/@OverridepublicSetdoScan(String...basePackages){//调用父类的扫描逻辑 , 转换为BeanDefinitionHolderSetbeanDefinitions=super.doScan(basePackages);if(beanDefinitions.isEmpty()){.....忽略不必要代码.....}else{//为这些接口的逻辑设置beanClassprocessBeanDefinitions(beanDefinitions);}//返回这些设置好的包装类returnbeanDefinitions;}无可厚非 , 我们肯定先进入到super.doScan(basePackages)方法!
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan源码解读
/***在指定的基本软件包中执行扫描 , *返回注册的bean定义 。 *此方法不会注册注释配置处理器而是将其留给调用方 。 *@parambasePackages包以检查带注释的类*@return为工具注册目的而已注册的一组bean(决不{@codenull})*/protectedSetdoScan(String...basePackages){.....忽略不必要代码.....SetbeanDefinitions=newLinkedHashSet<>();for(StringbasePackage:basePackages){//查找候选组件主要是查找spring的bean完成扫描的这个是将传入的包路径下的类(符合条件的)转换成对应的bdSetcandidates=findCandidateComponents(basePackage);.....忽略不必要代码.....}//返回本次经过全部流程扫描的beanreturnbeanDefinitions;}这个代码篇幅原因我忽略了不少 , 具体源码注释如下:
/***这个就是扫描过滤转换class成bd的地方*@parambasePackage包路径*@return转换成功的bd*/privateSetscanCandidateComponents(StringbasePackage){Setcandidates=newLinkedHashSet<>();try{//拼装一个扫描的路径StringpackageSearchPath=ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+resolveBasePackage(basePackage)+'/'+this.resourcePattern;//这一步做了递归拿到所有的类 , 这一步读取了配置类里面配置的路径文件//然后通过包名以及io手段将包名替换成文件夹的全路径 , 通过递归拿到里面所有的类文件Resource[]resources=getResourcePatternResolver().getResources(packageSearchPath);.....忽略不必要代码.....//这里开始将对应的类资源文件转换成对应的bdfor(Resourceresource:resources){.....忽略不必要代码.....if(resource.isReadable()){try{MetadataReadermetadataReader=getMetadataReaderFactory().getMetadataReader(resource);//这一步是扫描判断过滤器的//可以通过addIncludeFilter添加一些匹配规则//这个就是我们前面添加到的过滤器 , 不然的话在这里就不会生效//也不会添加到容器中if(isCandidateComponent(metadataReader)){//构建一个扫描bean的定义ScannedGenericBeanDefinitionsbd=newScannedGenericBeanDefinition(metadataReader);//设置源sbd.setResource(resource);sbd.setSource(resource);//这一步是判断这个是不是接口等可以由子类复写//这个判断也很重要 , 下面一张图会详细解释if(isCandidateComponent(sbd)){if(debugEnabled){logger.debug("Identifiedcandidatecomponentclass:"+resource);}//确定是一个候选组件的话就把这个放到候选组件的集合里面candidates.add(sbd);}.....忽略不必要代码.....}.....忽略不必要代码.....}.....忽略不必要代码.....}.....忽略不必要代码.....}}catch(IOExceptionex){thrownewBeanDefinitionStoreException("I/Ofailureduringclasspathscanning",ex);}//返回筛选转换的候选beanreturncandidates;}上述代码片段中 , 第二段判断isCandidateComponent(sbd),只有它通过的时候 , 才会被加载到候选组件中 , 在Spring原本的逻辑中 , 他是不会被加载进来的 , 但是 , 因为MyBatis重写了这段逻辑 , 所以 , 他才会被加载 , 重写逻辑如下:
/***给扫描到的处理器设置一些自定义的属性*@parambeanDefinitions对应接口的beanDefinition*/privatevoidprocessBeanDefinitions(SetbeanDefinitions){GenericBeanDefinitiondefinition;for(BeanDefinitionHolderholder:beanDefinitions){.....忽略不必要代码.....//映射器接口是Bean的原始类但是 , bean的实际类是MapperFactoryBean//这里传入的是对应接口的全限定名 , 未来注入到mapperFactoryBean中后 , 会被自动的转换成classdefinition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);//设置对应的class , 细心点你会发现 , 他注入的属性并不是对应的接口 , 而是一个MapperFactoryBean.classdefinition.setBeanClass(this.mapperFactoryBeanClass);.....忽略不必要代码.....}}这一段逻辑特别重要 , 为什么呢?因为要知道我们扫描出来的bd都是接口类型的 , 在java中 , 接口是不能被实例化的 , 想要让Spring管理这些Mapper接口 , 那么Spring所实例化的必须是一个具体的类 , 所以 , 这里就注入了一个MapperFactoryBean , 他是FactoryBean类型的对象 , Spring后续在实例化这个Mapper接口的时候 , 会通过FactoryBean实例化!我们进入到MapperFactoryBean中查看对象!
在看这个之前 , 我们需要了解FactoryBean的最基础的知识 , 就是Spring在创建对象的时候 , 如果发现这个对象是一个FactoryBean类型的数据 , 那么会调用getObject方法 , 获取对应的对象 , 所以 , 我们只需要关注org.mybatis.spring.mapper.MapperFactoryBean#getObject方法 , 就可以看出Spring究竟是如何把一个接口变为具体的Mapper操作实现类的!
publicclassMapperFactoryBeanextendsSqlSessionDaoSupportimplementsFactoryBean{/***通过注入额mapperInterface全限定名 , 自动转换为class对象*/privateClassmapperInterface;.....忽略不必要代码...../***spring会回调这个方法获取最终的对象*@return要创建的对象*@throwsException异常*/@OverridepublicTgetObject()throwsException{returngetSqlSession().getMapper(this.mapperInterface);}/***要创建对象的类型*@return什么类型?*/@OverridepublicClassgetObjectType(){returnthis.mapperInterface;}/***是不是单例*@return是单例吗?*/@OverridepublicbooleanisSingleton(){returntrue;}.....忽略不必要代码.....}由此可见 , getObject通过getSqlSession调用MyBatis逻辑 , 使用jdk动态代理来实现对接口的转换操作的!
你明白了吗?
整个流程比较麻烦 , 我们用一张图解决下!
推荐阅读
- 养老金|2021年上半年办理退休,养老金核算的这些知识要把握
- 量化|量化大师麦教授:美好的不确定性
- 浪胃仙|泡泡龙的离世给所有吃播提了醒,浪胃仙顺势决定“转行”,新职业认真的吗?
- 脑梗死|脑梗死和喝酒有没有关系呢?爱喝酒的朋友,应该看看
- 米歇尔·戴斯玛克特|海奥华预言的真相,地球人被带到九级文明,揭开神话背后的秘密
- 减肥也能吃的小零食,营养美味,低脂低热量,多吃也不怕!
- 1碗面粉,不加水,锅里蒸一蒸,做香甜可口的发糕,比蛋糕还香
- 扇贝最好吃的做法,适合冬日里吃,做法简单好吃不腻,家人超爱吃
- 七种颜色的布丁吃过没有?软糯爽口,Q弹软糯
- 爱吃南瓜饼的收藏,外酥里嫩,香甜软糯,饭桌上必备,做法超简单
