Spring source code series - how is Mybatis integrated into spring source code analysis

Posted by xtrafile on Sat, 06 Nov 2021 22:30:16 +0100

1, Spring startup process reanalysis

Configuration class

@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;
    }
}

Startup class

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. Handle the Import process

Spring source code series (IV) -- function analysis of configuration class postprocessor

https://blog.csdn.net/zxd1435513775/article/details/120935494?spm=1001.2014.3001.5501

In the above article, we analyzed parser.parse(candidates); This method of parsing the configuration class also focuses on how Spring parses and scans the @ ComponentScan annotation. This part of the core code is mainly in the doProcessConfigurationClass() method.

After Spring scans the @ ComponentScan annotation, it calls the following line of code processImports() to process the class imported by the @ Import annotation.

// Process @ Import annotation
processImports(configClass, sourceClass, getImports(sourceClass), true);

We run the program according to the above startup class and configuration class, and break to the line of processImports() method, as shown in the following figure. This sourceClass is the AppConfig.class configuration class. The getImports(sourceClass) method is to get the @ Import annotation information on this configuration class.

You can see from the breakpoint that the @ import (mappercannerregister. Class) information in the @ mappercan ("com. Scorpios. Mapper") annotation is obtained, and mappercannerregister implements the importbeandefinitionregister interface.

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar

We know that there are three ways to use @ Import annotation, one of which is to Import the ImportBeanDefinitionRegistrar implementation class.

@The Import annotation is to convert the Class class into a BeanDefinition, and then add the BeanDefinition to the Spring container

For the use of @ Import annotation, see the following article:

https://blog.csdn.net/zxd1435513775/article/details/100625879

Several ways to register components in the Spring container

Let's look at the method logic for handling @ Import annotation in the code:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
                            Collection<SourceClass> 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
                    // Handling the ImportSelector class
                    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<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                        processImports(configClass, currentSourceClass, importSourceClasses, false);
                    }
                } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                    // Handling ImportBeanDefinitionRegistrar class
                    // The importbeandefinitionregister class exposes BeanDefinitionRegistry
                    // This BeanDefinitionRegistry is that BeanDefinition can be added to the spring container
                    Class<?> candidateClass = candidate.loadClass();
                    // Instantiate and create objects without putting them into bdMap. They will be called directly later. Mybatis and Spring integration are introduced here
                    ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
                    ParserStrategyUtils.invokeAwareMethods( registrar, this.environment, this.resourceLoader, this.registry);
                    configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                } else {
                    // Dealing with general classes
                    // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                    // process it as an @Configuration class
                    this.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                    // Here is another recursive call to parse the import ed class again
                    processConfigurationClass(candidate.asConfigClass(configClass));
                }
            }
        } catch (BeanDefinitionStoreException ex) {
            throw ex;
        } catch (Throwable ex) {
            
        } finally {
            this.importStack.pop();
        }
    }
}

The above method for handling the ImportBeanDefinitionRegistrar class has only four lines:

  • Get org.mybatis.spring.annotation.MapperScannerRegistrar class
  • Instantiate mappercannerregister
  • Put the instantiated mappercannerregister into the importBeanDefinitionRegistrars attribute in the ConfigurationClass converted by AppConfig, and then take it from this Map and execute it

Note: the object created by mappercannerregister here is not placed in bdMap, but in the importBeanDefinitionRegistrars attribute in the ConfigurationClass class. It will be called directly later. This is where Mybatis and Spring are integrated!!!!
The ImportBeanDefinitionRegistrar of AOP is also imported here!!!

2. Execute Import class

The above has completed the scanning of ImportBeanDefinitionRegistrars interface classes and instantiated them, but we know that these classes are not converted to BeanDefinition, but instantiated directly== Where to call the method of the interface class== The answer is in parser.parse(candidates); Line 9 below this line of code is where the break point is shown in the figure below. this.reader.loadBeanDefinitions() is called in this method.

From the above analysis, we know that the classes that implement the importbeandefinitionregistrars interface will be placed in the importbeandefinitionregistrars of the Map attribute of the ConfigurationClass. At the above breakpoint, we need to take out these interface implementation classes from the Map, go through them one by one, and make method calls.

Let's look at the processing logic of this method:

public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
    TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
    for (ConfigurationClass configClass : configurationModel) {
        // Take a look at this method
        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;
    }

    // If a class is import ed, it will be marked by spring and registered here
    if (configClass.isImported()) {
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    
    // Here is the method to handle the @ Bean annotation modification in the configuration class
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }

    // Handle the resources imported by @ ImportResource on the configuration class and the bean s in xml
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    
    // The ImportBeanDefinitionRegistrar interface implementation class that handles the @ Import import Import on the configuration class
    // Call the registerBeanDefinitions() method in the ImportBeanDefinitionRegistrar interface
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

The above is the method to handle the @ Bean annotation in the configuration class, and the following is the method to implement the ImportBeanDefinitionRegistrar interface class. The entrance of Mybatis integration is right here!!!!

2, The entry class MapperScannerRegistrar of Mybatis

From the above analysis, the program calls the registerBeanDefinitions() method in the mappercannerregister class, which is provided by Mybatis and the entry point for Spring to integrate Mybatis.

// Notice the second parameter, BeanDefinitionRegistry
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    // Note the value of annoAttrs returned according to MapperScan annotation
    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    
    // Create a ClassPathMapperScanner scanner that inherits the ClassPathBeanDefinitionScanner
    // Be familiar with the ClassPathBeanDefinitionScanner. Spring uses this scanner to complete scanning
    // Note that the scanner here has a spring container, that is, registry, so you can put things into the spring container
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    // The following series of codes assign some properties to the scanner
    if (resourceLoader != null) {
        scanner.setResourceLoader(resourceLoader);
    }

    Class<? extends Annotation> 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<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
    if (!BeanNameGenerator.class.equals(generatorClass)) {
        scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
    }

    Class<? extends MapperFactoryBean> 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<String> basePackages = new ArrayList<String>();
    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();
    // Start scanning the path specified by @ MapperScan("com.scorpios.mapper")
    scanner.doScan(StringUtils.toStringArray(basePackages));
}

Note here that according to the @ MapperScan annotation, the returned value of annoAttrs, where the attribute value of factorybean is a factorybean

The last line of the above method is the scanner.doScan() method. Make a breakpoint to see the specific situation in the scanner:

Note that the scanner scanner here has a Spring container, namely registry, so that the scanned class can be transformed into BeanDefinition and then put into the Spring container

1. scanner.doScan()

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // Call the doScan() method of the parent class. At this time, basePackages is the package specified by the @ MapperScan annotation
    // Convert the scanned class to BeanDefinition and return
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
		// Print code omitted
    } else {
        // Process the scanned and converted BeanDefinition
        processBeanDefinitions(beanDefinitions);
    }
    return beanDefinitions;
}

According to the above analysis, the ClassPathMapperScanner scanner inherits the ClassPathBeanDefinitionScanner, so calling the doScan() method of the parent class here is to call the doScan() method in the ClassPathBeanDefinitionScanner class.

Take a closer look at the ClassPathBeanDefinitionScanner. Isn't it Spring's scanner when scanning packages? Indeed, this place even calls the same method. So here we go through the package scanning process of Spring again, but the path of scanning the package is the value specified by the @ MapperScan annotation.

So here comes the question...?

Since the Spring package scanning process is repeated here, why didn't you scan these Mapper files in the first scan? How does Spring determine whether a class meets the conditions when scanning packages? Let's analyze it.

Let's debug one by one. The breakpoint is set in the ClassPathScanningCandidateComponentProvider#scanCandidateComponents() method.

The following figure is the result of Spring's first self scanning of the com.scorpios package. We can see that all classes under the com.scorpios package will be taken into the resource array, and then traversed one resource by one to see whether the conditions are met and converted into BeanDefinition

Let's take a look at how to judge whether it conforms to the transformation of a class into a BeanDefinition. The method is isCandidateComponent() at the break point in the figure above. Let's take a look at the method code:

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.excludeFilters) {
        // Whether it is within the exclusion range and meets the direct return
        if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return false;
        }
    }
    for (TypeFilter tf : this.includeFilters) {
        // If the match is successful, it is marked as meeting the condition, and then the class is converted to BeanDefinition
        if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return isConditionMatch(metadataReader);
        }
    }
    return false;
}

Let's take a look at this TypeFilter, because the difference between the two scans is here!!!!!

In this.excludeFilters, match() is judged in the AbstractTypeHierarchyTraversingFilter#match() method.

The matchSelf() method is also in the AbstractTypeHierarchyTraversingFilter class. This method directly returns false, indicating dissatisfaction.

protected boolean matchSelf(MetadataReader metadataReader) {
    return false;
}

The judgment of match() in this.includeFilters is also in the AbstractTypeHierarchyTraversingFilter#match() method.

But the matchSelf() method here is in the AnnotationTypeFilter class

// The judgment here is whether there is @ Component annotation on the class. If so, it returns true
protected boolean matchSelf(MetadataReader metadataReader) {
    // When you get the annotation information, metadata stores the annotation information
    AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
    
    return metadata.hasAnnotation(this.annotationType.getName()) ||
        (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}

Here you know why Spring can scan which classes for the first time.

Next, let's analyze the packet scanning triggered for the second time from Mybatis:

The scanned package path is com.scorpios.mapper, which is the location of the interface file. As can be seen from the above figure, the Mapper file is obtained.

Let's verify whether this file needs to be converted to BeanDefinition

Next comes the match() method, but the match() method at this time is in the classpathmappercanner class, and the code returns true, so all scanned files here meet the requirements and will be converted to BeanDefinition

It should be noted here that the beanClass attribute in BeanDefinition is now com.scorpios.mapper.StudentMapper

But it will be replaced by org.mybatis.spring.mapper.MapperFactoryBean

Where is this replacement?

The answer is processBeanDefinitions() in the ClassPathMapperScanner class

Why do you need to replace it?

2. processBeanDefinitions()

// The input parameter of this method is the collection of scanned Mapper files into BeanDefinition
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    
    // BeanDefinition scanned circularly
    for (BeanDefinitionHolder holder : beanDefinitions) {
        definition = (GenericBeanDefinition) holder.getBeanDefinition();

       // The mapper interface is the original class of the bean, but the actual class of the bean is MapperFactoryBean 
        definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
        
        // Convert all Mapper classes scanned to BeanDefinition, and set the class in BeanDefinition to 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;
        }
        // Set the type of automatic injection and inject by type
        if (!explicitFactoryUsed) {
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
    }
}

This is the end of the analysis. How Spring integrates the Mybatis framework and where it replaces interfaces to classes.

3, Summary

Spring integrates Mybatis mainly through @ mappercan annotation. The principle behind this annotation is to Import mappercannerregister class into the container through @ Import, which implements the ImportBeanDefinitionRegistrar interface.

By analyzing how the @ Import annotation handles the class source code that implements the ImportBeanDefinitionRegistrar interface, this paper is familiar with the integration process of Mybatis and knows the location of replacing the interface with MappperFactoryBean.

In fact, the implementation principle of AOP in Spring is also the ImportBeanDefinitionRegistrar interface, which will be analyzed in the next article.

Topics: Mybatis Spring source code analysis