牛逼哄哄的Spring是怎么被MyBatis给征服了-!

本文详细解读了Spring中的MapperScannerConfigurer如何构建自定义扫描器,注册BeanDefinition,以及如何通过registerFilters方法配置扫描策略,重点关注了ClassPathMapperScanner的scan方法和filter设置,展示了FactoryBean在将接口映射到具体实现类中的作用。
摘要由CSDN通过智能技术生成

…忽略不必要代码…
//向这个bd里面注入一个 basePackage 属性,未来可以通过属性注入的方式注入到 MapperScannerConfigurer 的属性中
builder.addPropertyValue(“basePackage”, StringUtils.collectionToCommaDelimitedString(basePackages));
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

}
…忽略不必要代码…
}

这一段代码最终的逻辑简单来说就是构建了一个自定义扫描器MapperScannerConfigurer然后注册到Bean工厂中,他也就是前面术语项中说的BeanDefinitionRegistryPostProcessor的实现类,Spring声明周期中,会自动回调postProcessBeanDefinitionRegistry()方法,进行一系列的操作。我们下一步就是进入到MapperScannerConfigurer中看一下他做了哪些操作!

public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
/**

  • 自定义扫描器
  • @param registry 注册到bean工厂的工具类
    */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
    processPropertyPlaceHolders();
    }
    //构建一个自定义的扫描器 他是 ClassPathBeanDefinitionScanner 的子类
    // 可以扫描项目下的class文件转换成BeanDefinition
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    …忽略不必要代码…
    //这一步是很重要的,他是注册了一系列的过滤器,使得Spring在扫描到Mapper接口的时候不被过滤掉
    scanner.registerFilters();
    //开始执行扫描程序 传入对应要扫描的包路径
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
    }

这一段代码主要是在Spring回调这个方法后,这个方法会构建一个ClassPathMapperScanner扫描器,他是前面术语项中说到的ClassPathBeanDefinitionScanner的子类实现,然后调用 ClassPathMapperScannerscan方法,将扫描到的类转换成对应的BeanDefinition注册到容器中,正常来说我们应该关注的是scan方法,但是但是,我们在看scan之前,应该重点的关注一下registerFilters方法,我们大可看一下他做了哪些操作!然后再去看scan方法!

/**

  • 配置父扫描程序以搜索正确的界面。它可以搜索所有接口或仅搜索那些
  • 扩展了markerInterface或/和那些用notificationClass注释的标记
    */
    public void registerFilters() {
    boolean acceptAllInterfaces = true;

// 如果指定指定注解标注的Mapper
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}

// 指定接口的Mapper接口
if (this.markerInterface != null) {
addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
@Override
protected boolean matchClassName(String className) {
return false;
}
});
acceptAllInterfaces = false;
}
//默认的添加所有的Mapper接口为MyBatis类
if (acceptAllInterfaces) {
// 默认包含所有类的过滤器
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}

// 排除package-info.java
addExcludeFilter((metadataReader, metadataReaderFactory) -> {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith(“package-info”);
});
}

为什么要先看这个呢?因为对于Spring而言,他对一个BeanDefinition有着很严格的校验,当扫描的类不符合预定的一些条件的时候,Spring就会把它丢弃掉,不会管理这个类,我们这个方法就是为了,让Spring在扫描到那些接口的时候,添加一些自定义的过滤器,使Spring能够识别我们预定的这些接口,然后转换成BeanDefinition!

自定义的过滤器添加完毕后,我们就进入到scan方法去!

/**

  • 在指定的基本程序包中执行扫描。
  • @param basePackages 包以检查带注释的类
  • @return 注册的bean的数量
    */
    public int scan(String… basePackages) {
    //获取现有的总数 bd
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    //开始扫描逻辑
    doScan(basePackages);
    …忽略不必要代码…
    //统计本次扫描新增加的BeanDefinition数量 使用总共的数量 - 原本的数量
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
    }

这一步没的说,他会统计一下本次新加的一个bd的数量,我们进入到scan方法

/**

  • 调用父级搜索,该搜索将搜索并注册所有候选者。然后注册的对象处理以将它们设置为MapperFactoryBeans
  • @param basePackages 要扫描的包路径
  • @return 对应的BeanDefinition的包装类
    */
    @Override
    public Set doScan(String… basePackages) {
    //调用父类的扫描逻辑,转换为 BeanDefinitionHolder
    Set beanDefinitions = super.doScan(basePackages);
    if (beanDefinitions.isEmpty()) {
    …忽略不必要代码…
    } else {
    //为这些接口的逻辑设置beanClass
    processBeanDefinitions(beanDefinitions);
    }
    //返回这些设置好的包装类
    return beanDefinitions;
    }

无可厚非,我们肯定先进入到super.doScan(basePackages)方法!

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan 源码解读

/**

  • 在指定的基本软件包中执行扫描,
  • 返回注册的bean定义。
  • 此方法不会注册注释配置处理器而是将其留给调用方。
  • @param basePackages 包以检查带注释的类
  • @return 为工具注册目的而已注册的一组bean(决不{@code null})
    */
    protected Set doScan(String… basePackages) {
    …忽略不必要代码…
    Set beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
    //查找候选组件主要是查找spring的bean 完成扫描的 这个是将传入的包路径下的类(符合条件的) 转换成对应的bd
    Set candidates = findCandidateComponents(basePackage);
    …忽略不必要代码…
    }
    //返回本次经过全部流程扫描的bean
    return beanDefinitions;
    }

这个代码篇幅原因我忽略了不少,具体源码注释如下:

扫描逻辑

当然,我们最需要关注的就是 findCandidateComponents(basePackage)方法,他是真正的扫描逻辑,真正的将一个class行对象变为BeanDefinition

扫描逻辑包装方法

不想复制了,直接截图,理所应当的进入到了scanCandidateComponents方法:

/**

  • 这个就是扫描 过滤 转换 class成bd的地方
  • @param basePackage 包路径
  • @return 转换成功的bd
    */
    private Set scanCandidateComponents(String basePackage) {
    Set candidates = new LinkedHashSet<>();
    try {
    //拼装一个扫描的路径
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
    resolveBasePackage(basePackage) + ‘/’ + this.resourcePattern;
    //这一步做了递归拿到所有的类,这一步读取了配置类里面配置的路径文件
    //然后通过包名以及io手段将包名替换成文件夹的全路径,通过递归拿到里面所有的类文件
    Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
    …忽略不必要代码…
    //这里开始将对应的类资源文件转换成对应的bd
    for (Resource resource : resources) {
    …忽略不必要代码…
    if (resource.isReadable()) {
    try {
    MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
    //这一步是扫描判断过滤器的
    //可以通过 addIncludeFilter 添加一些匹配规则
    //这个就是我们前面添加到的过滤器,不然的话在这里就不会生效
    //也不会添加到容器中
    if (isCandidateComponent(metadataReader)) {
    //构建一个扫描bean的定义
    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
    //设置源
    sbd.setResource(resource);
    sbd.setSource(resource);
    //这一步是判断这个是不是 接口等 可以由子类复写
    //这个判断也很重要,下面一张图会详细解释
    if (isCandidateComponent(sbd)) {
    if (debugEnabled) {
    logger.debug("Identified candidate component class: " + resource);
    }
    //确定是一个候选组件的话就把这个放到候选组件的集合里面
    candidates.add(sbd);
    }
    …忽略不必要代码…
    }
    …忽略不必要代码…
    }
    …忽略不必要代码…
    }
    …忽略不必要代码…
    }
    }
    catch (IOException ex) {
    throw new BeanDefinitionStoreException(“I/O failure during classpath scanning”, ex);
    }
    //返回 筛选转换的候选bean
    return candidates;
    }

上述代码片段中,第二段判断isCandidateComponent(sbd),只有它通过的时候,才会被加载到候选组件中,在Spring原本的逻辑中,他是不会被加载进来的,但是,因为MyBatis重写了这段逻辑,所以,他才会被加载,重写逻辑如下:

判断类是否加载进验证逻辑

至此,我们的接口被扫描出来,并转换成了 BeanDefinition,我们逐步返回到最终的调用逻辑org.mybatis.spring.mapper.ClassPathMapperScanner#doScan中:

原始调用逻辑

我们将上一步扫描到的 BeanDefinitionHolder 使用箭头所指的方法设置了一些属性,什么属性呢?

/**

  • 给扫描到的处理器设置一些自定义的属性
  • @param beanDefinitions 对应接口的 beanDefinition
    */
    private void processBeanDefinitions(Set beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
    …忽略不必要代码…
    // 映射器接口是Bean的原始类但是,bean的实际类是MapperFactoryBean
    //这里传入的是对应接口的全限定名,未来注入到 mapperFactoryBean中后,会被自动的转换成class
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
    //设置对应的class,细心点你会发现,他注入的属性并不是对应的接口,而是一个 MapperFactoryBean.class
    definition.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操作实现类的!

public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean {
/**

  • 通过注入额 mapperInterface全限定名,自动转换为class对象
    */
    private Class mapperInterface;

…忽略不必要代码…

/**

  • spring会回调这个方法获取最终的对象
  • @return 要创建的对象
  • @throws Exception 异常
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

至此,文章终于到了尾声。总结一下,我们谈论了简历制作过程中需要注意的以下三个部分,并分别给出了一些建议:

  1. 技术能力:先写岗位所需能力,再写加分能力,不要写无关能力;
  2. 项目经历:只写明星项目,描述遵循 STAR 法则;
  3. 简历印象:简历遵循三大原则:清晰,简短,必要,要有的放矢,不要海投;

以及最后为大家准备的福利时间:简历模板+Java面试题+热门技术系列教程视频

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
放矢,不要海投;

以及最后为大家准备的福利时间:简历模板+Java面试题+热门技术系列教程视频

[外链图片转存中…(img-5hjeOKpx-1713754923288)]

[外链图片转存中…(img-gr1VYCo7-1713754923288)]

[外链图片转存中…(img-WpC8hgYn-1713754923289)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值