Spring源码解析11——整合MyBatis

  MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由Apache Software Foundation 迁移到了 Google Code,并且改名为 MyBatis。
  MyBatis是支持普通 SQL查询、存储过程和高级映射的优秀持久层框架。MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。

1、Spring整合MyBatis

  • Spring的配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
">


    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="1"/>
        <property name="maxTotal" value="300"/>
        <property name="maxIdle" value="2"/>
        <property name="maxWaitMillis" value="500"/>
        <property name="defaultAutoCommit" value="true"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis/Mybatis-Configuration.xml"></property>
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--有2种配置方式,都可以使用-->
    <!--配置方式一:-->
    <!--<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="com.xxx.mybatis.UserMapper"/>
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>-->

    <!--配置方式二:-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.xxx.mybatis"/>
    </bean>

</beans>
  • Mybatis配置文件Mybatis-Configuration.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias alias="User" type="com.xxx.mybatis.User"/>
    </typeAliases>
    <mappers>
        <mapper resource="mybatis/UserMapper.xml"/>
    </mappers>
</configuration>
  • dao层接口的映射文件UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mybatis.UserMapper">
    <insert id="insertUser" parameterType="User">
        insert into user(name,age,sex) values(#{name},#{age},#{sex})
    </insert>

    <select id="findAll" resultType="User">
        select * from user;
    </select>
</mapper>
  • dao层的接口UserMapper.interface
package com.xxx.mybatis;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("userMapper")
public interface UserMapper {
    public void insertUser(User user);

    public List<User> findAll();
}
  • POJO实体类User.class
package com.xxx.mybatis;
public class User {
//    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private int age;
    private String sex;

    public User() {
    }

    public User(int id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    //省略setter()、getter()、toString()
}
  • 在spring5.1.9源码的spring-jdbc/spring-jdbc.gradle文件中加入以下依赖
    implementation 'org.apache.commons:commons-dbcp2:2.9.0'//非spring源码内容
    implementation 'mysql:mysql-connector-java:5.1.6'//非spring源码内容
    implementation 'org.mybatis:mybatis:3.4.5'//非spring源码内容
    implementation 'org.mybatis:mybatis-spring:2.0.6'//非spring源码内容

加入后如下所示:
image

  • 在spring5.1.9源码的spring-core/spring-core.gradle文件中加入以下编译选项
    compile fileTree(dir: 'libs' ,include: '*.jar')

加入后如下所示:
image

  • 修改spring5.1.9源码的build.gradle文件,修改后如下
buildscript {
//    repositories {
//        gradlePluginPortal()
//        maven { url "https://repo.spring.io/plugins-release" }
//    }
//    dependencies {
//        classpath("io.spring.gradle:propdeps-plugin:0.0.9.RELEASE")
//        classpath("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16")
//    }
    ext {
        springBootVersion = '2.0.3.BUILD-SNAPSHOT'
    }
    repositories {
        mavenCentral()
        maven { url 'https://repo.spring.io/snapshot' }
//        maven { url "https://repo.spring.io/plugins-release" }
        maven{ url "http://maven.aliyun.com/nexus/content/groups/public" }
        //阿里云新库
        maven { url "https://maven.aliyun.com/repository/central" }
        maven { url "https://maven.aliyun.com/repository/google" }
        maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
        maven { url "https://maven.aliyun.com/repository/jcenter" }
        maven { url "https://maven.aliyun.com/repository/spring" }
        maven { url "https://maven.aliyun.com/repository/spring-plugin" }
        maven { url "https://maven.aliyun.com/repository/public" }
        maven { url "https://maven.aliyun.com/repository/releases" }
        maven { url "https://maven.aliyun.com/repository/snapshots" }
        maven { url "https://maven.aliyun.com/repository/grails-core" }
        maven { url "https://maven.aliyun.com/repository/mapr-public" }
        maven { url "http://repo.springsource.org/plugins-release" }
    }
    dependencies {
        classpath("io.spring.gradle:propdeps-plugin:0.0.9.RELEASE")
//        classpath("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16")
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
    }
}

// 3rd party plugin repositories can be configured in settings.gradle
plugins {
    id "io.spring.dependency-management" version "1.0.7.RELEASE" apply false
    id "org.jetbrains.kotlin.jvm" version "1.2.71" apply false
    id "org.jetbrains.dokka" version "0.9.18"
    id "org.asciidoctor.convert" version "1.5.8"
}

ext {
    linkHomepage = "https://spring.io/projects/spring-framework"
    linkCi = "https://build.spring.io/browse/SPR"
    linkIssue = "https://github.com/spring-projects/spring-framework/issues"
    linkScmUrl = "https://github.com/spring-projects/spring-framework"
    linkScmConnection = "scm:git:git://github.com/spring-projects/spring-framework.git"
    linkScmDevConnection = "scm:git:ssh://git@github.com:spring-projects/spring-framework.git"

    moduleProjects = subprojects.findAll {
        !it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom")
    }

    aspectjVersion       = "1.9.6"
    freemarkerVersion    = "2.3.28"
    groovyVersion        = "2.5.9"
    hsqldbVersion        = "2.4.1"
    jackson2Version      = "2.9.9"
    jettyVersion         = "9.4.31.v20200723"
    junit5Version        = "5.3.2"
    kotlinVersion        = "1.2.71"
    log4jVersion         = "2.11.2"
    nettyVersion         = "4.1.51.Final"
    reactorVersion       = "Californium-SR23"
    rxjavaVersion        = "1.3.8"
    rxjavaAdapterVersion = "1.2.1"
    rxjava2Version       = "2.2.19"
    slf4jVersion         = "1.7.30"      // spring-jcl + consistent 3rd party deps
    tiles3Version        = "3.0.8"
    tomcatVersion        = "9.0.37"
    undertowVersion      = "2.0.32.Final"

    gradleScriptDir = "${rootProject.projectDir}/gradle"
    withoutJclOverSlf4j = {
        exclude group: "org.slf4j", module: "jcl-over-slf4j"
    }
}

configure(allprojects) { project ->
    group = "org.springframework"
    version = qualifyVersionIfNecessary(version)

    apply plugin: "java"
    apply plugin: "kotlin"
    apply plugin: "checkstyle"
    apply plugin: "propdeps"
    apply plugin: "test-source-set-dependencies"
    apply plugin: "io.spring.dependency-management"
    apply from: "${gradleScriptDir}/ide.gradle"

    dependencyManagement {
        resolutionStrategy {
            cacheChangingModulesFor 0, "seconds"
        }
        applyMavenExclusions = false
        generatedPomCustomization {
            enabled = false
        }
    }

    configurations.all {
        // Check for updates every build
        resolutionStrategy.cacheChangingModulesFor 0, "seconds"

        // Consistent slf4j version (e.g. clashes between slf4j versions)
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == "org.slf4j") {
                details.useVersion slf4jVersion
            }
        }
    }

    def commonCompilerArgs =
            ["-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
             "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
             "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options"]

    compileJava.options*.compilerArgs = commonCompilerArgs +
            ["-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes",
             "-Xlint:deprecation", "-Xlint:unchecked", "-Werror"]

    compileTestJava.options*.compilerArgs = commonCompilerArgs +
            ["-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
             "-Xlint:-deprecation", "-Xlint:-unchecked"]

    compileJava {
        sourceCompatibility = 1.8  // can be switched to 11 for testing
        targetCompatibility = 1.8
        options.encoding = "UTF-8"
    }

    compileTestJava {
        sourceCompatibility = 1.8  // can be switched to 11 for testing
        targetCompatibility = 1.8
        options.encoding = "UTF-8"
        options.compilerArgs += "-parameters"
    }

    compileKotlin {
        kotlinOptions {
            jvmTarget = "1.8"
            freeCompilerArgs = ["-Xjsr305=strict"]
            apiVersion = "1.1"
            languageVersion = "1.1"
        }
    }

    compileTestKotlin {
        kotlinOptions {
            jvmTarget = "1.8"
            freeCompilerArgs = ["-Xjsr305=strict"]
        }
    }

    test {
        systemProperty("java.awt.headless", "true")
        systemProperty("testGroups", project.properties.get("testGroups"))
        systemProperty("io.netty.leakDetection.level", "paranoid")
        scanForTestClasses = false
        include(["**/*Tests.class", "**/*Test.class"])
        // Since we set scanForTestClasses to false, we need to filter out inner
        // classes with the "$" pattern; otherwise, using -Dtest.single=MyTests to
        // run MyTests by itself will fail if MyTests contains any inner classes.
        exclude(["**/Abstract*.class", '**/*$*'])
    }

    checkstyle {
        toolVersion = "8.38"
        configDir = rootProject.file("src/checkstyle")
    }

    repositories {
        //本地仓库,地址是maven本地仓库路径
//        mavenLocal()
        //maven私服。此处设置为ali的旧库,地址是url
        maven{ url "http://maven.aliyun.com/nexus/content/groups/public" }
        //阿里云新库
        maven { url "https://maven.aliyun.com/repository/central" }
        maven { url "https://maven.aliyun.com/repository/google" }
        maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
        maven { url "https://maven.aliyun.com/repository/jcenter" }
        maven { url "https://maven.aliyun.com/repository/spring" }
        maven { url "https://maven.aliyun.com/repository/spring-plugin" }
        maven { url "https://maven.aliyun.com/repository/public" }
        maven { url "https://maven.aliyun.com/repository/releases" }
        maven { url "https://maven.aliyun.com/repository/snapshots" }
        maven { url "https://maven.aliyun.com/repository/grails-core" }
        maven { url "https://maven.aliyun.com/repository/mapr-public" }
        maven { url "http://repo.springsource.org/plugins-release" }

    }

    dependencies {
        testCompile("junit:junit:4.12") {
            exclude group: "org.hamcrest", module: "hamcrest-core"
        }
        testCompile("org.mockito:mockito-core:2.28.2") {
            exclude group: "org.hamcrest", module: "hamcrest-core"
        }
        testCompile("com.nhaarman:mockito-kotlin:1.6.0") {
            exclude module: "kotlin-stdlib"
            exclude module: "kotlin-reflect"
            exclude module: "mockito-core"
        }
        testCompile("org.hamcrest:hamcrest-all:1.3")
        testRuntime("org.apache.logging.log4j:log4j-core:${log4jVersion}")
        testRuntime("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
        testRuntime("org.apache.logging.log4j:log4j-jul:${log4jVersion}")
        // JSR-305 only used for non-required meta-annotations
        compileOnly("com.google.code.findbugs:jsr305:3.0.2")
        testCompileOnly("com.google.code.findbugs:jsr305:3.0.2")
        checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.7")
    }

    ext.javadocLinks = [
            "https://docs.oracle.com/javase/8/docs/api/",
            "https://docs.oracle.com/javaee/7/api/",
            "https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/",  // CommonJ
            "https://www.ibm.com/support/knowledgecenter/SS7JFU_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/",
            "https://glassfish.java.net/nonav/docs/v3/api/",
            "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/",
            "https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/",
            "https://tiles.apache.org/tiles-request/apidocs/",
            "https://tiles.apache.org/framework/apidocs/",
            "https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
            "https://www.ehcache.org/apidocs/2.10.4/",
            "https://www.quartz-scheduler.org/api/2.3.0/",
            "https://fasterxml.github.io/jackson-core/javadoc/2.9/",
            "https://fasterxml.github.io/jackson-databind/javadoc/2.9/",
            "https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.9/",
            "https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/",
            "https://junit.org/junit4/javadoc/4.12/",
            "https://junit.org/junit5/docs/${junit5Version}/api/"
    ] as String[]
}

configure(subprojects - project(":spring-build-src")) { subproject ->
    apply from: "${gradleScriptDir}/publish-maven.gradle"

    jar {
        manifest.attributes["Implementation-Title"] = subproject.name
        manifest.attributes["Implementation-Version"] = subproject.version
        manifest.attributes["Automatic-Module-Name"] = subproject.name.replace('-', '.')  // for Jigsaw
        manifest.attributes["Created-By"] =
                "${System.getProperty("java.version")} (${System.getProperty("java.specification.vendor")})"

        from("${rootProject.projectDir}/src/docs/dist") {
            include "license.txt"
            include "notice.txt"
            into "META-INF"
            expand(copyright: new Date().format("yyyy"), version: project.version)
        }
    }

    javadoc {
        description = "Generates project-level javadoc for use in -javadoc jar"

        options.encoding = "UTF-8"
        options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED
        options.author = true
        options.header = project.name
        options.use = true
        options.links(project.ext.javadocLinks)
        options.addStringOption("Xdoclint:none", "-quiet")

        // Suppress warnings due to cross-module @see and @link references.
        // Note that global 'api' task does display all warnings.
        logging.captureStandardError LogLevel.INFO
        logging.captureStandardOutput LogLevel.INFO  // suppress "## warnings" message
    }

    task sourcesJar(type: Jar, dependsOn: classes) {
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        classifier = "sources"
        from sourceSets.main.allSource
        // Don't include or exclude anything explicitly by default. See SPR-12085.
    }

    task javadocJar(type: Jar) {
        classifier = "javadoc"
        from javadoc
    }

    artifacts {
        archives sourcesJar
        archives javadocJar
    }
}

configure(rootProject) {
    description = "Spring Framework"

    apply plugin: "groovy"
    apply from: "${gradleScriptDir}/jdiff.gradle"
    apply from: "${gradleScriptDir}/docs.gradle"

    dependencyManagement {
        imports {
            mavenBom "io.projectreactor:reactor-bom:${reactorVersion}"
        }
    }

    // Don't publish the default jar for the root project
    configurations.archives.artifacts.clear()

    dependencies {  // for integration tests
        testCompile(project(":spring-aop"))
        testCompile(project(":spring-beans"))
        testCompile(project(":spring-context"))
        testCompile(project(":spring-core"))
        testCompile(project(":spring-expression"))
        testCompile(project(":spring-jdbc"))
        testCompile(project(":spring-orm"))
        testCompile(project(":spring-test"))
        testCompile(project(":spring-tx"))
        testCompile(project(":spring-web"))
        testCompile("javax.inject:javax.inject:1")
        testCompile("javax.resource:javax.resource-api:1.7.1")
        testCompile("javax.servlet:javax.servlet-api:3.1.0")
        testCompile("org.aspectj:aspectjweaver:${aspectjVersion}")
        testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
        testCompile("org.hibernate:hibernate-core:5.1.17.Final")
    }

    artifacts {
        archives docsZip
        archives schemaZip
        archives distZip
    }

    wrapper {
        doLast() {
            def gradleOpts = "-XX:MaxMetaspaceSize=1024m -Xmx1024m"
            def gradleBatOpts = "$gradleOpts -XX:MaxHeapSize=256m"
            File wrapperFile = file("gradlew")
            wrapperFile.text = wrapperFile.text.replace("DEFAULT_JVM_OPTS=",
                    "GRADLE_OPTS=\"$gradleOpts \$GRADLE_OPTS\"\nDEFAULT_JVM_OPTS=")
            File wrapperBatFile = file("gradlew.bat")
            wrapperBatFile.text = wrapperBatFile.text.replace("set DEFAULT_JVM_OPTS=",
                    "set GRADLE_OPTS=$gradleBatOpts %GRADLE_OPTS%\nset DEFAULT_JVM_OPTS=")
        }
    }
}

/*
 * Support publication of artifacts versioned by topic branch.
 * CI builds supply `-P BRANCH_NAME=<TOPIC>` to gradle at build time.
 * If <TOPIC> starts with 'SPR-', change version
 *     from BUILD-SNAPSHOT => <TOPIC>-SNAPSHOT
 *     e.g. 3.2.1.BUILD-SNAPSHOT => 3.2.1.SPR-1234-SNAPSHOT
 */
def qualifyVersionIfNecessary(version) {
    if (rootProject.hasProperty("BRANCH_NAME")) {
        def qualifier = rootProject.getProperty("BRANCH_NAME")
        if (qualifier.startsWith("SPR-")) {
            return version.replace("BUILD", qualifier)
        }
    }
    return version
}
  • 数据库表结构和表中的数据
    image
    image

  • 测试

package com.xxx.mybatis;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
public class Spring_Mybatis_Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("mybatis/applicationContext.xml");
        UserMapper userMapper = (UserMapper)context.getBean("userMapper");
        List<User> list = userMapper.findAll();
        System.out.println(list);
    }
}

执行结果如下:
image

2、源码分析

  通过分析上文整合示例中的applicationContext.xml配置文件,我们可以知道配置的bean其实是成树状结构的,而在树的最顶层是类型为org.mybatis.spring.SqlSessionFactoryBean的bean,它将其他相关 bean 组装在了一起,那么,我们的分析就从此类开始。

2.1、创建SqlSessionFactory

  Spring应该通过org.mybatis.spring.SqlSessionFactoryBean 封装了 MyBatis 中的实现。org.mybatis.spring.SqlSessionFactoryBean的UML图如下所示:
image
其中有2个重要的接口InitializingBean.interface和FactoryBean.interface,功能如下:

  • InitializingBean:实现此接口的 bean 会在初始化时调用其 afterPropertiesSet() 函数来进行 bean 的逻辑初始化。
  • FactoryBean:一旦某个 bean 实现此接口,那么通过 getBean 方法获取 bean 时其实是获取此类的 getObject()函数返回的实例。
2.1.1、通过实现InitializingBean.interface::afterPropertiesSet() 函数来初始化SqlSessionFactory

SqlSessionFactory.interface和所有实现类的的UML图如下所示:
image

SqlSessionFactoryBean.class::afterPropertiesSet()

  private SqlSessionFactory sqlSessionFactory;

  @Override
  public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
        "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }
  
  protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      targetConfiguration = this.configuration;
      if (targetConfiguration.getVariables() == null) {
        targetConfiguration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        targetConfiguration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
      LOGGER.debug(
          () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      targetConfiguration = new Configuration();
      Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }

    Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
    Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
    Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);

    if (hasLength(this.typeAliasesPackage)) {
      scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
          .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
          .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
    }

    if (!isEmpty(this.typeAliases)) {
      Stream.of(this.typeAliases).forEach(typeAlias -> {
        targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
        LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
      });
    }

    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    if (hasLength(this.typeHandlersPackage)) {
      scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
          .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
          .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }

    targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);

    if (!isEmpty(this.scriptingLanguageDrivers)) {
      Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
        targetConfiguration.getLanguageRegistry().register(languageDriver);
        LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
      });
    }
    Optional.ofNullable(this.defaultScriptingLanguageDriver)
        .ifPresent(targetConfiguration::setDefaultScriptingLanguage);

    if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
      try {
        targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

    Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

    if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();
        LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    targetConfiguration.setEnvironment(new Environment(this.environment,
        this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
        this.dataSource));

    if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }

  此函数主要目的就是对于sqlSessionFactory的初始化,通过之前展示的独立使用MyBatis 的示例,我们了解到 SqlSessionFactory 是所有 MyBatis 功能的基础。
  从这个函数中可以得知,配置文件还可以支持其他多种属性的配置,如configLocation、objectFactory、objectWrapperFactory、typeAliasesPackage、typeAliases、typeHandlersPackage、plugins、typeHandlers 、transactionFactory、databaseldProvider、mapperLocations。
  其实,如果只按照常用的配置,那么我们只需要在函数最开始按照如下方式处理configuration :

      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      targetConfiguration = xmlConfigBuilder.getConfiguration();

根据 configLocation 构造 XMLConfigBuilder 并进行解析,但是,为了体现 Spring 更强大的兼容性,Spring 还整合了 MyBatis 中其它属性的注人,并通过实例 configuration 来承载每一步所获取的信息并最终使用 sqlSessionFactoryBuilder 实例根据解析到的configuration 创建SqlSessionFactory实例。

2.1.2、通过实现FactoryBean.interface::getObject()函数获取SqlSessionFactory实例

SqlSessionFactoryBean.class::getObject()

  private SqlSessionFactory sqlSessionFactory;
  @Override
  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }
2.2、创建MapperFactoryBean

上文中的测试类Spring_Mybatis_Test.class还可以用SqlSessionFactory来实现,如下所示:

package com.xxx.mybatis;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
public class Spring_Mybatis_Test {
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("mybatis/applicationContext.xml");
//        UserMapper userMapper = (UserMapper)context.getBean("userMapper");
        SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)context.getBean("sqlSessionFactory");
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> list = userMapper.findAll();
        System.out.println(list);
    }
}

其余配置和class不变,执行结果与上文中的结果相同。
  为了使用 MyBatis 功能,示例中的 Spring 配置文件提供了两个 bean,除了之前分析的SqlSssionFactoryBean 类型的 bean 以外,还有一个是 MapperFactoryBean 类型的 bean。MapperFactoryBean会在

sqlSession.getMapper(UserMapper.class)

中使用,MapperFactoryBean.class的UML图,如下:
image
同样的,其中有2个重要的接口InitializingBean.interface和FactoryBean.interface,功能如下:

  • InitializingBean:实现此接口的 bean 会在初始化时调用其 afterPropertiesSet() 函数来进行 bean 的逻辑初始化。
  • FactoryBean:一旦某个 bean 实现此接口,那么通过 getBean 方法获取 bean 时其实是获取此类的 getObject()函数返回的实例。
2.2.1、通过实现InitializingBean.interface::afterPropertiesSet() 函数来初始化MapperFactoryBean

  因为实现了 InitializingBean.interface接口,Spring 会保证在 bean 初始化时首先调用 afterPropertiesSet()函数来完成其初始化逻辑。追踪父类,发现aferPropertiesSet()函数是在DaoSupport.class中实现
DaoSupport.class::afterPropertiesSet()

    @Override
    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        // Let abstract subclasses check their configuration.
        checkDaoConfig();

        // Let concrete implementations initialize themselves.
        try {
            initDao();
        }
        catch (Exception ex) {
            throw new BeanInitializationException("Initialization of DAO failed", ex);
        }
    }

但从函数名称来看我们大体推测,MapperFactoryBean 的初始化包括对 DAO 配置的验证以及对 DAO的初始工作,其中initDao()函数是模板函数,设计为留给子类做进一步逻辑处理。而 checkDaoConfg()才是我们分析的重点。
MapperFactoryBean.class::checkDaoConfig()

  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();//调用SqlSessionDaoSupport.class中的checkDaoConfig()函数

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        //此步是将UserMapper.class注册到类型中
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

SqlSessionDaoSupport.class::checkDaoConfig()

  @Override
  protected void checkDaoConfig() {
    notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
  }

结合代码我们了解到对于 DAO 配置的验证,Spring 做了以下几个方面的工作。
①、父类中对于sqlSession不为空的验证:
sqlSession作为根据接口创建映射器代理的接触类一定不可以为空,而sqlSession 的初始化工作是在设定其 sqlSessionFactory 属性时完成的,如下:
SqlSessionDaoSupport.class

  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }

  protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }

也就是说,对于下面的配置如果忽略了对于sqlSessionFactory属性的设置,那么在此时就会被检测出来。

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="com.xxx.mybatis.UserMapper"/>
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

②、映射接口的验证
接口是映射器的基础,sqlSession会根据接口动态创建相应的代理类,所以接口必不可少。
③、映射文件存在性验证
对于函数前半部分的验证我们都很容易理解,无非是对配置文件中的属性是否存在做验证,但是后面部分是完成了什么方面的验证呢?如果读者读过 MyBatis 源码,你就会知道,在 MyBatis 实现过程中并没有手动调用 configuration.addMapper()函数,而是在映射文件读取过程中一旦解析到如<mapper namespace="Mapper.UserMapper">,便会自动进行类型映射的注册。那么,Spring 中为什么会把这个功能单独拿出来放在验证里呢?这是不是多此一举呢?
在上面的函数中,configuration.addMapper(this.mapperlnterface)其实就是将 UserMapper 注册到映射类型中,如果你可以保证这个接口一定存在对应的映射文件,那么其实这个验证并没有必要。但是,由于这个是我们自行决定的配置,无法保证这里配置的接口一定存在对应的映射文件,所以这里非常有必要进行验证。在执行此代码的时候,MyBatis 会检查嵌人的映射接口是否存在对应的映射文件,如果没有会抛出异常,Spring正是在用这种方式来完成接口对应的映射文件存在性验证。

2.2.2、通过实现FactoryBean.interface::getObject()函数获取MapperFactoryBean

MapperFactoryBean.class::getObject()

  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

这段代码正是我们在提供MyBatis独立使用的时候的一个代码调用。Spring通过FactoryBean 进行了封装。

2.3、MapperScannerConfigurer

  上文applicationContext.xml文件中的配置方式二就是使用的MapperScannerConfigurer,配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
">


    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="1"/>
        <property name="maxTotal" value="300"/>
        <property name="maxIdle" value="2"/>
        <property name="maxWaitMillis" value="500"/>
        <property name="defaultAutoCommit" value="true"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis/Mybatis-Configuration.xml"></property>
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--有2种配置方式,都可以使用-->
    <!--配置方式一:-->
    <!--<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="com.xxx.mybatis.UserMapper"/>
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>-->

    <!--配置方式二:-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.xxx.mybatis"/>
    </bean>

</beans>

basePackage 属性是让你为映射器接口文件设置基本的包路径。你可以使用分号或逗号作为分隔符设置多于一个的包路径。每个映射器将会在指定的包路径中递归地被搜索到。被发现的映射器将会使用Spring对自动侦测组件默认的命名策略来命名。也就是说,如果没有发现注解,它就会使用映射器的非大写的非完全限定类名。但是如果发现了@Component或JSR-330@Named 注解,它会获取名称。
通过上面的配置,Spring就会帮助我们对com.xxx.mybatis目录下面的所有接口进行自动的注人而不需要为每个接口重复在 Spring 配置文件中进行声明了。那么,这个功能又是如何做到的呢?MapperScannerConfigurer 中又有哪些核心操作呢?同样,首先查看MapperScannerConfigurer的UML图,如下:
image
其中有2个重要的接口,BeanDefinitionRegistryPostProcessor.interface和BeanFactoryPostProcessor.interface,功能如下:

  • BeanDefinitionRegistryPostProcessor:实现此接口的 bean 会在初始化时调用其 postProcessBeanDefinitionRegistry() 函数
  • BeanFactoryPostProcessor:实现此接口的 bean 会在初始化时调用其postProcessBeanFactory() 函数

主要看实现了BeanDefinitionRegistryPostProcessor.interface接口的postProcessBeanDefinitionRegistry() 函数,如下:
MapperScannerConfigurer.class::postProcessBeanDefinitionRegistry()

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();//2.3.1、processPropertyPlaceHolders属性的处理
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    scanner.registerFilters();// 2.3.2、根据配置属性生成过滤器
    //2.3.3、扫描Java文件
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
2.3.1、processPropertyPlaceHolders属性的处理

MapperScannerConfigurer.class::processPropertyPlaceHolders()

  /*在应用程序启动之初,就会先调用 BeanDefinitionRegistries ,而在此之前并不会调用 BeanFactoryPostProcessors 。
  * 这意味着 PropertyResourceConfigurers 尚未被加载,因此该类属性的任何替换操作都将失败。
  * 为避免这种情况,需找到上下文中定义的任何 PropertyResourceConfigurers,并在该类的 bean 定义上运行它们。然后更新这些值。
  */
  private void processPropertyPlaceHolders() {
    Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class,
        false, false);

    if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
      BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory()
          .getBeanDefinition(beanName);

      // PropertyResourceConfigurer does not expose any methods to explicitly perform
      // property placeholder substitution. Instead, create a BeanFactory that just
      // contains this mapper scanner and post process the factory.
      DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
      factory.registerBeanDefinition(beanName, mapperScannerBean);

      for (PropertyResourceConfigurer prc : prcs.values()) {
        prc.postProcessBeanFactory(factory);
      }

      PropertyValues values = mapperScannerBean.getPropertyValues();

      this.basePackage = getPropertyValue("basePackage", values);
      this.sqlSessionFactoryBeanName = getPropertyValue("sqlSessionFactoryBeanName", values);
      this.sqlSessionTemplateBeanName = getPropertyValue("sqlSessionTemplateBeanName", values);
      this.lazyInitialization = getPropertyValue("lazyInitialization", values);
      this.defaultScope = getPropertyValue("defaultScope", values);
    }
    this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
    this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName)
        .map(getEnvironment()::resolvePlaceholders).orElse(null);
    this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName)
        .map(getEnvironment()::resolvePlaceholders).orElse(null);
    this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders)
        .orElse(null);
    this.defaultScope = Optional.ofNullable(this.defaultScope).map(getEnvironment()::resolvePlaceholders).orElse(null);
  }

回顾这个函数所做的事:
①、找到所有已经注册的PropertyResourceConfigurer 类型的bean。
②、模拟 Spring 中的环境来用处理器。这里通过使用new DefaultListableBeanFactory()来模拟 Spring 中的环境(完成处理器的调用后便失效),将映射的bean,也就是 MapperScannerConfigurer 类型 bean 注册到环境中来进行后处理器的调用,处理器 PropertyResourceConfigurer调用完成的功能,即找出所有bean中应用属性文件的变量并替换。也就是说,在处理器调用后,模拟环境中模拟的 MapperScannerConfgurer 类型的 bean 如果有引人属性文件中的属性那么已经被替换了,这时,再将模拟 bean 中相关的属性提取出来应用在真实的 bean 中。

2.3.2、根据配置属性生成过滤器

  在 postProcessBeanDefinitionRegistry()函数中可以看到,配置中支持很多属性的设定,但是我们感兴趣的或者说影响扫描结果的并不多,属性设置后通过在 scanner.registerFilters()函数中生成对应的过滤器来控制扫描结果。
ClassPathMapperScanner.class::registerFilters()

  public void registerFilters() {
    boolean acceptAllInterfaces = true;

    // if specified, use the given annotation and / or marker interface
    //对于annotationClass属性的处理
    if (this.annotationClass != null) {
      addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
      acceptAllInterfaces = false;
    }

    // override AssignableTypeFilter to ignore matches on the actual marker interface
    //对于markerInterface属性的处理
    if (this.markerInterface != null) {
      addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
        @Override
        protected boolean matchClassName(String className) {
          return false;
        }
      });
      acceptAllInterfaces = false;
    }

    if (acceptAllInterfaces) {
      // default include filter that accepts all classes
      addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
    }

    // exclude package-info.java
    //不扫描package-info.java文件
    addExcludeFilter((metadataReader, metadataReaderFactory) -> {
      String className = metadataReader.getClassMetadata().getClassName();
      return className.endsWith("package-info");
    });
  }

代码中得知,根据之前属性的配置生成了对应的过滤器
①、annotationClass 属性处理。
如果 annotationClass 不为空,表示用户设置了此属性,那么就要根据此属性生成过滤器以保证达到用户想要的效果,而封装此属性的过滤器就是AnnotationTypeFilter。AnnotationTypeFilter 保证在扫描对应 Java 文件时只接受标记有注解为 @annotationClass 的接口。
②、markerInterface 属性处理。
如果 markerInterface 不为空,表示用户设置了此属性,那么就要根据此属性生成过滤器以保证达到用户想要的效果,而封装此属性的过滤器就是实现AssignableTypeFilter 接口的局部类。表示扫描过程中只有实现markernterface 接口的接口才会被接受。
③、全局默认处理。在上面两个属性中如果存在其中任何属性,acceptAllInterfaces的值将会被改变,但是如果用户没有设定以上两个属性,那么,Spring 会为我们增加一个默认的过滤器实现 TypeFilter 接口的局部类,旨在接受所有接口文件。
④、package-info.java处理
对于命名为package-info的Java文件,默认不作为逻辑实现接口,将其排除掉,使用TypeFilter 接口的局部类实现 match 方法。
  从上面的函数我们看出,控制扫描文件 Spring 通过不同的过滤器完成,这些定义的过滤器记录在了 includeFilters 和excludeFilters属性中,如下:
ClassPathScanningCandidateComponentProvider.class

    public void addIncludeFilter(TypeFilter includeFilter) {
        this.includeFilters.add(includeFilter);
    }

    /**
     * Add an exclude type filter to the <i>front</i> of the exclusion list.
     */
    public void addExcludeFilter(TypeFilter excludeFilter) {
        this.excludeFilters.add(0, excludeFilter);
    }

ClassPathMapperScanner.class和ClassPathScanningCandidateComponentProvider.class的UML关系图,如下所示:
image

2.3.3、扫描Java文件

  设置了相关属性以及生成了对应的过滤器后便可以进行文件的扫描了,扫描工作是由ClassPathMapperScanner.class 类型的实例scanner中完成的,实现函数为scan()(scan()函数在ClassPathMapperScanner.class的父类ClassPathBeanDefinitionScanner.class中实现)。如下:
ClassPathBeanDefinitionScanner.class::scan()

    public int scan(String... basePackages) {
        int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

        doScan(basePackages);
        //如果配置了includeAnnotationConfig,则注册对应注解的处理器以保证注解功能的正常使用
        // Register annotation config processors, if necessary.
        if (this.includeAnnotationConfig) {
            AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
        }

        return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
    }

scan() 是个全局函数,扫描工作通过 doScan(basePackages)委托给了 doScan() 函数,同时,还包括了includeAnnotationConfig 属性的处理,AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)代码主要是完成对于注解处理器的简单注册,比如AutowiredAnnotationBeanPostProcessor、RequiredAnnotationBeanPostProcessor等,这里不再赘述,我们重点研究文件扫描功能的实现。

  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
          + "' package. Please check your configuration.");
    } else {
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }
  
  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();
      boolean scopedProxy = false;
      if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
        definition = (AbstractBeanDefinition) Optional
            .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
            .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
                "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
        scopedProxy = true;
      }
      String beanClassName = definition.getBeanClassName();
      LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
          + "' mapperInterface");

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
      definition.setBeanClass(this.mapperFactoryBeanClass);

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      // Attribute for MockitoPostProcessor
      // https://github.com/mybatis/spring-boot-starter/issues/475
      definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

      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)) {
        if (explicitFactoryUsed) {
          LOGGER.warn(
              () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate",
            new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          LOGGER.warn(
              () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }

      definition.setLazyInit(lazyInitialization);

      if (scopedProxy) {
        continue;
      }

      if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
        definition.setScope(defaultScope);
      }

      if (!definition.isSingleton()) {
        BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
        if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
          registry.removeBeanDefinition(proxyHolder.getBeanName());
        }
        registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
      }

    }
  }

  此时,虽然还没有完全进入到扫描的过程,但是我们也应该理解了Spring 中对于自动扫描的注册,声明 MapperScannerConfigurer类型的bean 目的是不需要我们对于每个接口都注册一个 MapperFactoryBean 类型的对应的 bean,但是,不在配置文件中注册并不代表这个 bean不存在,而是在扫描的过程中通过编码的方式动态注册。实现过程我们在ClassPathBeanDefinitionScanner.class::doScan()函数中可以看得非常清楚。如下:
ClassPathBeanDefinitionScanner.class::doScan()

    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            //扫描basePackage路径下java文件
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition candidate : candidates) {
                //解析scope属性
                ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
                candidate.setScope(scopeMetadata.getScopeName());
                String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
                if (candidate instanceof AbstractBeanDefinition) {
                    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
                }
                if (candidate instanceof AnnotatedBeanDefinition) {
                    //如果是AnnotatedBeanDefinition.class类型的bean,需要检测下常用注解,如:Primary、Lazy等
                    AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
                }
                //检测当前bean是否已经注册
                if (checkCandidate(beanName, candidate)) {
                    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    //如果当前bean是用于生成代理的bean,那么需要进一步处理
                    definitionHolder =
                            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                    beanDefinitions.add(definitionHolder);
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
    }
    

findCandidateComponents()函数根据传入的包路径信息并结合类文件路径拼接成文件的绝对路径,同时完成了文件的扫描过程并且根据对应的文件生成了对应的bean,使用ScannedGenericBeanDefinition类型的 bean承载信息,bean中只记录了resource 和 source 信息。该函数在ClassPathBeanDefinitionScanner.class的父类ClassPathScanningCandidateComponentProvider.class中实现,如下:
ClassPathScanningCandidateComponentProvider.class::findCandidateComponents()

    public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
            return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
        }
        else {
            return scanCandidateComponents(basePackage);
        }
    }
    
    private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            Set<String> types = new HashSet<>();
            for (TypeFilter filter : this.includeFilters) {
                String stereotype = extractStereotype(filter);
                if (stereotype == null) {
                    throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
                }
                types.addAll(index.getCandidateTypes(basePackage, stereotype));
            }
            boolean traceEnabled = logger.isTraceEnabled();
            boolean debugEnabled = logger.isDebugEnabled();
            for (String type : types) {
                MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type);
                if (isCandidateComponent(metadataReader)) {
                    AnnotatedGenericBeanDefinition sbd = new AnnotatedGenericBeanDefinition(
                            metadataReader.getAnnotationMetadata());
                    if (isCandidateComponent(sbd)) {
                        if (debugEnabled) {
                            logger.debug("Using candidate component class from index: " + type);
                        }
                        candidates.add(sbd);
                    }
                    else {
                        if (debugEnabled) {
                            logger.debug("Ignored because not a concrete top-level class: " + type);
                        }
                    }
                }
                else {
                    if (traceEnabled) {
                        logger.trace("Ignored because matching an exclude filter: " + type);
                    }
                }
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }
        return candidates;
    }

  这里,我们更感兴趣的是 isCandidateComponent(metadataReader),此句代码用于判断当前扫描的文件是否符合要求,而我们之前注册的一些过滤器信息也正是在此时派上用场的。

    protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        for (TypeFilter tf : this.excludeFilters) {
            if (tf.match(metadataReader, getMetadataReaderFactory())) {
                return false;
            }
        }
        for (TypeFilter tf : this.includeFilters) {
            if (tf.match(metadataReader, getMetadataReaderFactory())) {
                return isConditionMatch(metadataReader);
            }
        }
        return false;
    }

  我们看到了之前加人过滤器的两个属性 excludeFilters、includeFilters,并且知道对应的文件是否符合要求是根据过滤器中的 match ()函数所返回的信息来判断的,当然用户可以实现并注册满足自己业务逻辑的过滤器来控制扫描的结果,metadataReader 中有你过滤所需要的全部文件信息。至此,我们完成了文件的扫描过程的分析。

posted @ 2026-01-02 20:47  Carey_ccl  阅读(4)  评论(0)    收藏  举报