從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

吉米同志發表於2018-12-20

搭建原始碼環境

在這裡我提一下,在早期Mybatis版本中,Dao開發方式都是有Mapper介面和其實現類的,實現類是需要我們自己編寫的,後來Mybatis使用JDK動態代理針對Mapper介面做了代理,替我們實現了實現類; 但是其底層也是使用了Mapper介面的實現類,不可能說只有一個即可就能和JDBC進行通訊 ! 其基礎環境搭建可參照官方教程 www.mybatis.org/mybatis-3/z… 本章末尾最後給出了本章中的測試環境

快速進入Debug跟蹤

我們可以在此處打上斷點,Debug模式啟動進入斷點,再按F7跟蹤入其方法

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

原始碼分析準備

在進行Mybatis的初始化過程之前,我們需要把整個大綱拎出來放在前面,讓大家能夠有所瞭解,然後在進行每個步驟的時候心裡有個大概;

  • 什麼是Mybatis的初始化過程?

從程式碼上來看 "SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);" 這行程式碼就是執行的是Mybatis的初始化操作,這個操作通常在應用中只會操作一次,構建完成SqlSessionFactory就不再使用,而SqlSessionFactory會跟隨整個應用生命週期;

從應用階段上來說 : Mybatis根據全域性XML配置檔案生成SqlSessionFactory的過程就是Mybatis的初始化過程.

  • 淺析一詞含義

既然標題為淺析某某....相比大家也能看出說明本章不會深度挖掘底層程式碼,我個人認為淺析一次的主要意義是 ""能夠快速地在我們心中建立底層原始碼的架構圖,快速瀏覽程式碼,與概念進行核對 "",當然也不包含某些大牛謙虛的說法哈~~ 在這裡提的主要目的是,本次淺析Mybatis是快速瀏覽程式碼; 稍後會出新的篇章對核心方法進行剖析

  • Mybatis初始化過程中的主要步驟
    • 將全域性配置檔案XML解析到Configuration物件
    • 將對映配置檔案XML解析到Configuration的mapperRegistry物件
    • 將對映配置檔案XML中的宣告(Statement)解析成MappedStatement物件存入Configuration物件的mappedStatements集合中
    • 最後將Configuration最為引數構建DefaultSqlSessionFactory物件

原始碼分析

第一步: 將全域性配置檔案XML載入到Configuration物件

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
parser.parse();
複製程式碼

主要功能 : 將全域性配置檔案中的配置載入到一個Configuration物件的屬性中

這是第一步,我們從Main方法的new SqlSessionFactoryBuilder().build(inputStream)進入斷點,可以看到在構建完畢SqlSessionFactoryBuilder物件後由呼叫了過載的build方法

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

//SqlSessionFactoryBuilder的構造方法
public SqlSessionFactoryBuilder() {
}

//build方法
public SqlSessionFactory build(InputStream inputStream) {
        return this.build((InputStream)inputStream, (String)null, (Properties)null);
 }

//build方法(過載)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            //第一步: 建立XML配置構建器,用來解析全域性XML檔案內容
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();
            try {
                inputStream.close();
            } catch (IOException var13) {
            }
        }
        return var5;
    }
複製程式碼

在繼續深入之前我們需要了解一下XMLConfigBuilder這個物件,從名字上來看就可以知道是解析XML配置檔案的;XMLConfigBuilder又繼承了BaseBuilder類,而在BaseBuilder類中有一個屬性Configuration,這個Configuration物件就是用來儲存全域性配置檔案和其他Mapper的配置資訊, 同時我們從下圖也可以看到XMLMapperBuilder,XMLStatementBuilder,MapperBuilderAssistant也繼承了BaseBuilder

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

XMLxxxBuilder是用來解析XML配置檔案的,不同型別XMLxxxBuilder用來解析MyBatis配置檔案的不同部位。

  • XMLConfigBuilder用來解析MyBatis的全域性配置檔案
  • XMLMapperBuilder用來解析MyBatis中的對映檔案
  • XMLStatementBuilder用來解析對映檔案中的statement語句。
  • MapperBuilderAssistant用來輔助解析對映檔案並生成MappedStatement物件

這些XMLxxxBuilder都有一個共同的父類——BaseBuilder。這個父類維護了一個全域性的Configuration物件,MyBatis的配置檔案解析後就以Configuration物件的形式儲存

看原始碼果然能發現貓膩,不錯不錯,可以看到在new這個XMLConfigBuilder物件的時候,下圖的斷點位置super(new Configuration());

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

可以看到Configuration的構造方法如下所示,這也正解釋了我們我們可以在全域性配置檔案中寫個JDBC就行,因為在Configuration物件在構建的時候就載入了一些預設的別名. 別告訴我你不知道別名是啥哈~~

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

public Configuration() {
        this.safeResultHandlerEnabled = true;
        this.multipleResultSetsEnabled = true;
        this.useColumnLabel = true;
        this.cacheEnabled = true;
        this.useActualParamName = true;
        this.localCacheScope = LocalCacheScope.SESSION;
        this.jdbcTypeForNull = JdbcType.OTHER;
        this.lazyLoadTriggerMethods = new HashSet(Arrays.asList("equals", "clone", "hashCode", "toString"));
        this.defaultExecutorType = ExecutorType.SIMPLE;
        this.autoMappingBehavior = AutoMappingBehavior.PARTIAL;
        this.autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
        this.variables = new Properties();
        this.reflectorFactory = new DefaultReflectorFactory();
        this.objectFactory = new DefaultObjectFactory();
        this.objectWrapperFactory = new DefaultObjectWrapperFactory();
        this.lazyLoadingEnabled = false;
        this.proxyFactory = new JavassistProxyFactory();
        this.mapperRegistry = new MapperRegistry(this);
        this.interceptorChain = new InterceptorChain();
        this.typeHandlerRegistry = new TypeHandlerRegistry();
        this.typeAliasRegistry = new TypeAliasRegistry();
        this.languageRegistry = new LanguageDriverRegistry();
        this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection");
        this.caches = new Configuration.StrictMap("Caches collection");
        this.resultMaps = new Configuration.StrictMap("Result Maps collection");
        this.parameterMaps = new Configuration.StrictMap("Parameter Maps collection");
        this.keyGenerators = new Configuration.StrictMap("Key Generators collection");
        this.loadedResources = new HashSet();
        this.sqlFragments = new Configuration.StrictMap("XML fragments parsed from previous mappers");
        this.incompleteStatements = new LinkedList();
        this.incompleteCacheRefs = new LinkedList();
        this.incompleteResultMaps = new LinkedList();
        this.incompleteMethods = new LinkedList();
        this.cacheRefMap = new HashMap();
        this.typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        this.typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
        this.typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        this.typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        this.typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        this.typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        this.typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        this.typeAliasRegistry.registerAlias("LRU", LruCache.class);
        this.typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        this.typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
        this.typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
        this.typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
        this.typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
        this.typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
        this.typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
        this.typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
        this.typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
        this.typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
        this.typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
        this.typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
        this.typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
        this.typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
        this.languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
        this.languageRegistry.register(RawLanguageDriver.class);
    }
複製程式碼

第一步還沒有執行完? 是的上述中我們在看構建XMLConfigBuilder物件過程,現在構建完成了我們就需要看這一行程式碼了parser.parse();; 當有了XMLConfigBuilder物件之後,接下來就可以用它來解析配置檔案了

   public Configuration parse() {
       //判斷是否已經解析,只能解析一次全域性配置檔案
        if (this.parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
            //將parsed標記為已經解析
            this.parsed = true;
            //解析全域性配置檔案的XML中的configuration節點
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            return this.configuration;
        }
    }
複製程式碼
//主要看一下解析全域性配置檔案的configuration節點的方法
private void parseConfiguration(XNode root) {
        try {
            //解析全域性配置檔案中的properties節點的配置資訊儲存到Configuration物件的variables屬性中
            this.propertiesElement(root.evalNode("properties"));
            //解析全域性配置檔案中的settings節點的配置資訊設定到Configuration物件的各個屬性中
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.settingsElement(settings);
           //解析全域性配置檔案中的typeAliases節點的配置資訊設定到BaseBuilder物件的TypeAliasRegistry屬性中
            this.typeAliasesElement(root.evalNode("typeAliases"));
            //解析全域性配置檔案的plugins
            this.pluginElement(root.evalNode("plugins"));
            //解析全域性配置檔案中的objectFactory設定到Configuration物件的objectFactory屬性中
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            //解析全域性配置檔案中的Environment節點儲存到Configuration物件中的Environment屬性中
            
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            //第二步 : 解析全域性配置檔案中的mappers節點  注意這是一個核心的方法 我們點進去看一下
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }
複製程式碼

從上述程式碼中可以看到,XMLConfigBuilder會依次解析配置檔案中的、、、、、等屬性。

第二步 : 解析對映配置檔案XML到Configuration的mapperRegistry容器

this.mapperElement(root.evalNode("mappers"));
複製程式碼

主要功能 : MyBatis會遍歷下所有的子節點,如果當前遍歷到的節點是,則MyBatis會將該包下的所有Mapper Class註冊到configuration的mapperRegistry容器中。如果當前節點為,則會依次獲取resource、url、class屬性,解析對映檔案,並將對映檔案對應的Mapper Class註冊到configuration的mapperRegistry容器中。

XMLConfigBuilder解析全域性配置檔案中有一個比較重要的一步;就是解析對映檔案this.mapperElement(root.evalNode("mappers"))這句程式碼開始解析對映檔案,我們開看一下下圖中構建了一個XMLMapperBuilder物件,這個物件是負責解析對映檔案的;而第一步的XMLConfigBuilder物件是解析全域性配置檔案的

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

上圖中紅色圈中的是Mybatis解析對映檔案的方法,我們進去看一下 mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());

  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
      //首先會初始化父類BaseBuilder,並將configuration賦給BaseBuilder;
        super(configuration);
      //然後建立MapperBuilderAssistant物件,該物件為XMLMapperBuilder的協助者,用來協助XMLMapperBuilder完成一些解析對映檔案的動作
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
      
        this.parser = parser;
        this.sqlFragments = sqlFragments;
        this.resource = resource;
    }

    public void parse() {
        if (!this.configuration.isResourceLoaded(this.resource)) {
            this.configurationElement(this.parser.evalNode("/mapper"));
            this.configuration.addLoadedResource(this.resource);
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }
複製程式碼

再看一下mapperParser.parse();

    public void parse() {
        //如果對映檔案沒有被載入過
        if (!this.configuration.isResourceLoaded(this.resource)) {
            //執行載入對映檔案XML方法configurationElement
            this.configurationElement(this.parser.evalNode("/mapper"));
            //將此對映檔案新增已經解析了的集合中
            this.configuration.addLoadedResource(this.resource);
            //繫結Namespace
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }
複製程式碼

下面是具體Mybatis解析對映檔案中的Statement的過程

   private void configurationElement(XNode context) {
        try {
            //獲取namespace
            String namespace = context.getStringAttribute("namespace");
            //判斷namespace,如果為空直接丟擲異常
            if (namespace != null && !namespace.equals("")) {
                //設定namespace
                this.builderAssistant.setCurrentNamespace(namespace);
                //下面就是解析各個Statement中的各個XML節點
                this.cacheRefElement(context.evalNode("cache-ref"));
                this.cacheElement(context.evalNode("cache"));
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                this.sqlElement(context.evalNodes("/mapper/sql"));
               
                //第三步 : 解析Statement宣告   核心方法
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            } else {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
        }
    }
複製程式碼

從上述程式碼中可以看到,XMLMapperBuilder藉助MapperBuilderAssistant會對Mapper對映檔案進行解析,在解析到最後,會將每一箇中的節點解析為MappedStatement物件

第三步 : 解析對映檔案的Statement為MappedStatement物件

this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

主要功能 : 將對映檔案的子節點解析為MappedStatement物件

我們進入 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));這個方法看一下

    private void buildStatementFromContext(List<XNode> list) {
        if (this.configuration.getDatabaseId() != null) {
            this.buildStatementFromContext(list, this.configuration.getDatabaseId());
        }

        this.buildStatementFromContext(list, (String)null);
    }

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        Iterator var3 = list.iterator();

        while(var3.hasNext()) {
            XNode context = (XNode)var3.next();
            XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);

            try {
                statementParser.parseStatementNode();
            } catch (IncompleteElementException var7) {
                this.configuration.addIncompleteStatement(statementParser);
            }
        }

    }
複製程式碼

其中主要的邏輯都在下示圖中的兩行程式碼中

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

接下來我們進入XMLStatementBuilder類的parseStatementNode去看看

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

最終由MapperBuilderAssistant完成MappedStatement物件的封裝,並且將MappedStatement物件放入Configuration物件的**mappedStatements**容器中

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

初始化完成

主要功能 : 將已經裝載了各種XML資訊的Configuration物件作為引數構建DefaultSqlSessionFactory返回,Mybatis初始化完成!!!

  public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
複製程式碼

從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程

搭建原始碼Debug環境

POM依賴

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>
複製程式碼

測試SQL

CREATE TABLE `user` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `username` varchar(10) NOT NULL,
  `password` varchar(52) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1
複製程式碼

Mybatis全域性配置檔案

<?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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="jimisun"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>
複製程式碼

UserMapper介面

public interface UserMapper {
    User selectUser(Integer id);
}
複製程式碼

UserMapper配置

<?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="user">
    <select id="selectUser" resultType="com.jimisun.mybatis.domain.User">
        select *
        from user
        where id = #{id}
    </select>
</mapper>
複製程式碼

User實體

public class User {
    private int id;
    private String username;
    private String password;
	getter and setter .....
}

複製程式碼

Main方法

    public static void main(String[] args) throws IOException {

        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = sqlSession.selectOne("user.selectUser", 2);
        System.out.println(user.toString());

    }
複製程式碼

該教程所屬Java工程師之Spring Framework深度剖析專欄,本系列相關博文目錄 Java工程師之Spring Framework深度剖析專欄

相關文章