- 一、Spring启动流程再分析
- 1. 处理Import过程
- 2. 执行Import导入类
- 二、Mybatis的入口类MapperScannerRegistrar
- 1. scanner.doScan()
- 2. processBeanDefinitions()
- 三、小结
配置类
@Configuration
@ComponentScan("com.scorpios")
@MapperScan("com.scorpios.mapper")
public class AppConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
return dataSource;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
}
启动类
public static void main( String[] args )
{
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(AppConfig.class);
ac.refresh();
StudentMapper studentMapper = (StudentMapper) ac.getBean("studentMapper");
System.out.println(studentMapper.selectAll());
}
1. 处理Import过程
Spring源码系列(四)——ConfigurationClassPostProcessor功能解析
https://blog.csdn.net/zxd1435513775/article/details/120935494?spm=1001.2014.3001.5501
在上面这篇文章中,我们分析了parser.parse(candidates);
这个解析配置类的方法,也重点分析了Spring
是如何解析并扫描@ComponentScan
注解的,这部分核心代码主要在doProcessConfigurationClass()
方法中。
当Spring
扫描完@ComponentScan
注解后,就去调用下面这行代码processImports()
来处理@Import
注解导入的类。
// 处理 @Import 注解
processImports(configClass, sourceClass, getImports(sourceClass), true);
我们按照上面的启动类和配置类来运行程序,并断点到processImports()
方法这一行,如下图。这个sourceClass
是AppConfig.class
配置类,getImports(sourceClass)
方法是拿到这个配置类上面的@Import
注解信息。
从断点处可以看到,拿到了@MapperScan("com.scorpios.mapper")
注解中的@Import(MapperScannerRegistrar.class)
信息,MapperScannerRegistrar
实现 ImportBeanDefinitionRegistrar
接口。
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar
我们知道@Import
注解的使用有三种方式,其中有一种使用方式是导入ImportBeanDefinitionRegistrar
实现类。
@Import注解就是把Class类转化为BeanDefinition,然后把这个BeanDefinition添加到Spring容器中
关于@Import注解的使用可以看下面这篇文章:
https://blog.csdn.net/zxd1435513775/article/details/100625879
向Spring容器中注册组件的几种方式
下面来看下代码中处理@Import
注解的方法逻辑:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection importCandidates, boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
} else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
// 处理ImportSelector类
Class candidateClass = candidate.loadClass();
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
this.deferredImportSelectors.add(
new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
} else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
} else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 处理ImportBeanDefinitionRegistrar类
// ImportBeanDefinitionRegistrar类暴露出BeanDefinitionRegistry
// 这个BeanDefinitionRegistry就是可以向spring容器中添加BeanDefinition
Class candidateClass = candidate.loadClass();
// 实例化,创建出来对象,并不需要放到bdMap中,后面直接调用,Mybatis与Spring集成就是在此处引入的
ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
ParserStrategyUtils.invokeAwareMethods( registrar, this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
} else {
// 处理普通类
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 此处又是一个递归调用,把import进来的类再次进行解析
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
} catch (BeanDefinitionStoreException ex) {
throw ex;
} catch (Throwable ex) {
} finally {
this.importStack.pop();
}
}
}
上面处理ImportBeanDefinitionRegistrar
类的方法只有四行:
- 获取到
org.mybatis.spring.annotation.MapperScannerRegistrar
类 - 实例化
MapperScannerRegistrar
- 把实例化好的
MapperScannerRegistrar
放入到由AppConfig
转化的ConfigurationClass
类中的importBeanDefinitionRegistrars
属性中,后面会从这个Map
中拿取并执行
注意:此处MapperScannerRegistrar
创建出来对象,并没有放到bdMap
中,而是放到了ConfigurationClass
类中的importBeanDefinitionRegistrars
属性中,后面会直接调用,Mybatis
与Spring
集成就是在此!!!! AOP
的ImportBeanDefinitionRegistrar
也是在此导入!!!
上面完成了对ImportBeanDefinitionRegistrars
接口类的扫描工作,并也已经实例化,但我们知道,并没有把这些类转为BeanDefinition
,而是直接进行了实例化。==那在哪里进行实现这个接口类方法的调用呢?==答案是在parser.parse(candidates);
这行代码的下面第9行,就是下图打断点的地方。this.reader.loadBeanDefinitions()
这个方法中进行了调用。
从上面的分析中,我们知道,实现ImportBeanDefinitionRegistrars
接口的类会被放到ConfigurationClass
的Map
属性的importBeanDefinitionRegistrars
中,上面这个断点处,就是要去这个Map
中拿出这些接口实现类,逐个遍历,并进行方法调用。
下面来看下这个方法的处理逻辑:
public void loadBeanDefinitions(Set configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
// 具体看下这个方法
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
// 如果一个类是被import,会被spring标注,在这里完成注册
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 此处是处理配置类中@Bean注解修饰的方法
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
// 处理配置类上@ImportResource导入的资源 ,xml中的bean
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
// 处理配置类上@Import导入的ImportBeanDefinitionRegistrar接口实现类
// 调用ImportBeanDefinitionRegistrar接口中的registerBeanDefinitions()方法
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
上面是处理配置类中具有@Bean
注解的方法,下面就是来处理实现ImportBeanDefinitionRegistrar
接口类的方法了。Mybatis
整合的入口就在这里!!!!
从上面的分析顺下来,程序调用到了MapperScannerRegistrar
类中的registerBeanDefinitions()
方法,这个类是由Mybatis
提供的,也是Spring
整合Mybatis
的入口。
// 注意下第二个参数BeanDefinitionRegistry
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 此处要注意下根据MapperScan注解,返回的annoAttrs的值
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
// 创建一个ClassPathMapperScanner扫描器,这个扫描器继承ClassPathBeanDefinitionScanner
// 这个ClassPathBeanDefinitionScanner扫描器熟悉吧,Spring完成扫描工作就是用的这个扫描器
// 注意一下,此处的扫描器是拥有spring容器的,即registry,这样就可以往spring容器中放东西了
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 下面一系列代码都是为这个scanner赋值一些属性
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
Class annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}
Class markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}
Class generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
}
Class mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
List basePackages = new ArrayList();
for (String pkg : annoAttrs.getStringArray("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (String pkg : annoAttrs.getStringArray("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class clazz : annoAttrs.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
scanner.registerFilters();
// 开始扫描@MapperScan("com.scorpios.mapper")指定的路径
scanner.doScan(StringUtils.toStringArray(basePackages));
}
此处要注意下根据@MapperScan
注解,返回的annoAttrs
的值,其中factoryBean
的属性值是个FactoryBean
上面方法的最后一行scanner.doScan()
方法,打个断点看下,这个scanner
扫描器里面的具体情况:
注意,此处的scanner扫描器是拥有Spring容器的,即registry,这样就可以把扫描到类转化为BeanDefinition,然后放入到Spring容器中了
public Set doScan(String... basePackages) {
// 调用父类的doScan()方法,此时basePackages是@MapperScan注解所指定的包
// 将扫描到的class转化为BeanDefinition返回
Set beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
// 打印代码略
} else {
// 处理扫描出来并转化成的BeanDefinition
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
由上面的分析知道,这个ClassPathMapperScanner
扫描器,继承ClassPathBeanDefinitionScanner
,所以此处调用父类的doScan()
方法就是调用ClassPathBeanDefinitionScanner
类中的doScan()
方法。
再仔细一看这个ClassPathBeanDefinitionScanner
,不就是Spring
自己扫描包时候的扫描器么?确实是的,这地方连调用的方法都是一样的。所以此处又把Spring
的包扫描流程走了一遍,只不过扫描包的路径是@MapperScan
注解指定的值了。
那么问题来了?
既然此处又把Spring
包扫描的流程走了一遍,那为什么在第一次扫描的时候,没有把这些Mapper
文件扫描进去呢?那Spring
进行包扫描的时候是怎么判断一个类是否符合条件呢?下面来分析一下。
下面来逐遍调试,断点打在ClassPathScanningCandidateComponentProvider#scanCandidateComponents()
方法。
下面这张图是Spring
第一次自己扫描com.scorpios
包的结果。我们可以看到,会把com.scorpios
包下面的所有类都拿到放到这个Resource
数组中,然后逐个resource
去遍历,看看是否满足条件转化为BeanDefinition
下面来看看是如何进行判断是不是符合将类转化为BeanDefinition
的,方法就是上图打断点的地方,isCandidateComponent()
,下面来看下这个方法代码:
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
// 是否在排除范围之内,满足直接返回
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
// 如果匹配成功,就标记为满足条件,然后把类转为BeanDefinition
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
下面要来看一下这个TypeFilter
,因为两次扫描的区别就在这!!!!!
this.excludeFilters
中match()
的判断是在AbstractTypeHierarchyTraversingFilter#match()
方法。
matchSelf()
方法也是在AbstractTypeHierarchyTraversingFilter
类中,这个方法直接返回的是false
,表示不满足。
protected boolean matchSelf(MetadataReader metadataReader) {
return false;
}
this.includeFilters
中match()
的判断也是在AbstractTypeHierarchyTraversingFilter#match()
方法。
但此处的matchSelf()
方法是在AnnotationTypeFilter
类中
// 此处的判断就是类上是否有@Component注解,如果有,则返回true
protected boolean matchSelf(MetadataReader metadataReader) {
// 拿到注解信息,metadata存放的就是注解信息
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
return metadata.hasAnnotation(this.annotationType.getName()) ||
(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}
这里知道为什么Spring
第一次扫描能扫描到哪些类了吧。
下面再来分析下从Mybatis
中第二次触发的包扫描:
扫描的包路径是com.scorpios.mapper
,即接口文件所在的位置。从上图中可以看出来拿到了Mapper
文件。
下面要来验证下这个文件是否需要转化为BeanDefinition
下面又到了match()
方法,但此时的match()
方法是在ClassPathMapperScanner
类中,而且代码中是返回的true
,所以此处的所有扫描文件都满足,都会被转化为BeanDefinition
此处需要注意的是,BeanDefinition里面的beanClass属性现在是com.scorpios.mapper.StudentMapper
但后面会被替换为org.mybatis.spring.mapper.MapperFactoryBean
这个替换的地方在哪呢?
答案在ClassPathMapperScanner类中的processBeanDefinitions()
为什么需要替换呢?
2. processBeanDefinitions()// 这个方法的入参是扫描到的Mapper文件转化为BeanDefinition的集合
private void processBeanDefinitions(Set beanDefinitions) {
GenericBeanDefinition definition;
// 循环扫描到的BeanDefinition
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 映射器接口是bean的原始类,但是bean的实际类是MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
// 将扫描到的所有Mapper类转化为BeanDefinition中的class设置为MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
// 设置自动注入的类型,按type进行注入
if (!explicitFactoryUsed) {
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
到此处就分析完了,Spring
是如何整合Mybatis
框架的了,而且在什么地方进行了从接口到类的替换。
Spring
整合Mybatis
主要就是通过@MapperScan
注解,该注解背后的原理就是通过@Import
向容器中导入MapperScannerRegistrar
类,而该类实现了ImportBeanDefinitionRegistrar
接口。
本文通过分析@Import
注解是如何处理实现ImportBeanDefinitionRegistrar
接口的类源代码,来熟悉Mybatis
的整合过程,并知道了将接口替换为MappperFactoryBean
的位置。
其实,Spring
中AOP
的实现原理也是这个ImportBeanDefinitionRegistrar
接口,留在下篇分析吧。