Mybati源碼分析(一) 創建SqlSessionFactory

簡介

Mybatis是一款優秀的ORM框架,是國內使用的ORM框架中的主流,它使用簡單,入們很快。對於有過幾年Java開發經驗的老司機,我相信都對它如何使用已經能夠輕車熟路了,但是,閱讀Mybatis的源碼的估計不多,如果各位朋友和要是和本人一樣,對閱讀源碼很感興趣,想要知道Mybatis廬山真面目的,可以一起探討探討。

Mybatis的使用的入口

我們先來看一下最簡單的查詢一個對象的實例 。

//讀取配置
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml"); 
//創建sqlSeessionFactory
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
//打開sqlSession
SqlSession ssn = ssf.openSession();
//查詢一個user對象
User user = ssn.selectOne("mapping.userMapper.getUserByName", "testUser");
//關閉會話
ssn.close();

上面的代碼是Mybatis最簡單的使用例子,麻雀雖小,五臟俱全,雖然只有短短5行代碼,它已經完整的包括了Mybatis的使用週期。Mybatis的週期從大體上可分爲三部分
在這裏插入圖片描述
我們要對Mybatis源碼解析,首先我們需要先列出Mybatis整個流程當中可能會用到的對象類型列出來,先弄清大體的框架,再去摳細節
在這裏插入圖片描述
上面的類型,其實還可以分得更簡單一些,分爲三個過程,再將每個類型對位到不同的過程,像下圖那樣
在這裏插入圖片描述

初始化

那麼如何從代碼的角度去分析mybatis的初始化過程,首先我用一個圖簡單表示一下需要用到的類型前後關係
在這裏插入圖片描述
Mybatis的使用的第一步是傳入根配置信息,我們拿最開頭的事例的頭兩行代碼出來看看

//Reader只是一個讀文件的接口,這裏還沒有開始讀
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml"); 
//將配置文入口傳給SqlSessionFactoryBuilder的實例方法
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);

我再點擊進去看一下SqlSessionFactoryBuilder的build()方法的實現

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

build代碼很簡單,它有幾個重載但沒有多大區別,我們看到new XMLConfigBuilder(reader, environment, properties),它有個reader參數就是我們在外面傳的配置文件入口,environment是指定的環境(是一個字符串),properties是系統配置信息,我們上面的例子中,沒有用到這兩個參數,所以傳的都是空。接下來,我們在看到build(parser.parse()),parse是解析的意思,它的作用是把配置信息解析成爲Configuration對象,然後再創建DefaultSqlSessionFactory對象,DefaultSqlSessionFactory有一個Configuration類型的參數的構造函數。

我們知道DefaultSqlSessionFactory是怎麼new來了,那我們再一探個究竟,看看DefaultSqlSessionFactory的部分源碼和SqlSessionFactory接口定義

public interface SqlSessionFactory {
  SqlSession openSession();
  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);
  Configuration getConfiguration();

}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
 private final Configuration configuration;
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
}

從上面的代碼可以看到SqlSessionFactory 的接口定義的都是openSession方法和getConfiguration方法,也就是說它的作用只有一個就是用來創建SqlSession對象,並且裏面存有一個Configuration的字段。我們再來看看DefaultSqlSessionFactory 的構造函數的實現,DefaultSqlSessionFactory 構造函數只做一件事情,就是保存configuration字段變量。

看了上面的代碼之後,其實我們還是不知道Mybatis怎麼初始化,因爲整個初始化的核心是其實不在SqlSessionFactoryBuiler上,而是在XMLConfigBuilder上。它纔是解析整個配置的核心,XMLConfigBuilder的作用是讀取根配置文件的配置信息,然後解析爲Java對象,最終將它放到Configuration對象裏面。

在講解XMLConfigBuilder之前,我們先來看一下Configuration類內部情況,因爲XMLConfigBuilder解析的過程離不開configuration對象

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
上面的代碼很多,但我們不需要細看每句代碼,從大體上看,它分爲各種配置字段和工廠方法,它的構造函數會初始化一部分配置。那爲什麼要把工廠方法也放到Configuration上呢? 其實這裏的工廠方法都是和配置相關的,例如創建MappedStatement對象,我們知道Mybatis的sql都是寫在Mapper配置裏面, 創建MappedStatement對象需要讀取配置信息,所以把工廠方法放到Configuration裏面也是合情合理的,所以不要把Configuration理解爲只有配置信息(其實還包括工廠方法,不過個人覺得Configuration和工廠方法放在一起耦合有點大,如果要自己寫個自定義Executor類型,想要簡單的加到Configuration上是沒有辦法的)。

接下來,我們來分析加載過程的真正核心,XMLConfigBuilder的內部實現。下面列出XMLConfigBuilder的部分源碼

public abstract class BaseBuilder {
  protected final Configuration configuration;
  protected final TypeAliasRegistry typeAliasRegistry;
  protected final TypeHandlerRegistry typeHandlerRegistry;
  
  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
 }

public class XMLConfigBuilder extends BaseBuilder {
 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()),    environment, props);
  }
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
}

上面列出了XMLConfigBuilder的構造函數和它的父類,先看構造函數,首先它new了一個configuration對象,然後調用父類的構造函數,父類的構造函數做了什麼呢?BaseBuilder它只有有三個字段成員,分別爲configuration ,typeAliasRegistry,typeHandlerRegistry。configuration是外面傳進來的,只需把它存起來就行了, typeAliasRegistry和typeHandlerRegistry是從configuration內部取出來的,它的作用是什麼了,看下面的截圖,其實不難猜到,typeAliasRegistry是一個別名(Alias)和類型(Type)的映射器(有多個別名對應一個type),因爲Java的基礎類型和數據庫的數據類型是不一樣的,所以下面TypeHandlerRegistry可以看到,每個基礎類型都註冊了一個Handler類型處理程序,所以它的作用就是用來處理類型轉換的。例如:我們從數據表裏面拿到了一個int類型的字段, 那我們就需要從TypeHandlerRegistry裏面找到一個IntegerTypeHandler對象,然後把它轉爲Java的Integer對象。現在調用完父類的構造函數後,接下來要處理的是設置environment (是環境的名稱,比如dev,prod等等)和系統環境參數props, 設置parsed爲false,最後設置parser解析器,parser的類型是XPathParser,它的作用是用來解析Xml文件。
在這裏插入圖片描述
在這裏插入圖片描述
接下來,我們再進一步分析,看看XMLConfigBuilder類裏面的parse()方法是怎麼解析的

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

首先執行parse方法時,需要判斷是否已經解析過,因爲Configuration實例的初始化不是冪等的,不能重複執行,不然會造成configuration配置重複的問題,接下來我們看到parser.evalNode("/configuration")方法,evalNode()方法的作用是找到Xml文件中的某個節點並將它轉爲XNode對象返回。
下面是一個簡單的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="dev">
        <environment id="dev">
            <transactionManager type="JDBC" />
            <!-- 配置數據庫連接信息 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://192.168.31.18:3306/UM?useUnicode=true&amp;characterEncoding=UTF-8" />
                <property name="username" value="root" />
                <property name="password" value="abc123" />
            </dataSource>
        </environment>
    </environments>
     <mappers>
         <mapper resource="mapping/userMapper.xml"/>
         <mapper class="mapping.account_mapper"/>
     </mappers>
</configuration>

通過parser.evalNode("/configuration")拿到了<configuration>節點信息之後,我們還需要給它解析下面的所有子節點.parse()方法裏面通過parseConfiguration()方法解析configuration節點的每個子節點。接下來我們繼續在XMLConfigBuilder類中找到parseConfiguration()方法的實現

  private void parseConfiguration(XNode root) {
    try {
      //解析properties節點
      propertiesElement(root.evalNode("properties"));
      //解析settings節點
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      //解析typeAliases節點
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析plugins節點
      pluginElement(root.evalNode("plugins"));
      //解析objectFactory節點
      objectFactoryElement(root.evalNode("objectFactory"));
      //解析objectWrapperFactory節點
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析reflectorFactory節點
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      //解析environments節點
      environmentsElement(root.evalNode("environments"));
      //解析databaseIdProvider節點
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //解析typeHandlers節點
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析mappers
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
 

上面看到configuration的子節點有十多種,每一種節點的特點和作用都不一樣,但節點的讀取統一都是evalNode()方法,不過拿到了它們的XNode節點之後,每個都是要通過不同的方式把它加到configuration裏面去的。這裏我們就只拿我們最關心的mapper節點去作介紹,Mybatis是怎麼將Mapper讀取到配置的,下面可以看mapperElement()方法的實現

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

parent參數對應的是的<mappers>節點(如下圖),由於mappers的是一個集合,所以上面的源碼對它的子節點進行了遍歷, maper節點有個name屬性,如果name等於"package"值時,它是一個包(就是把多個配置包在一起),其他不等於"package"的對應的都是一個單獨的Mapper。
在這裏插入圖片描述
如果節點的name屬性是package的話,那麼通過configuration.addMappers()方法直接加到configuration的配置裏面(在addMappers()方法裏面還會做進一步的解析)。

如果非package節點,又分爲三種情況:
1.只存在resource的屬性的節點:它是一個資源文件的地址,需要將它轉爲inputStream類型,再通過XMLMapperBuilder去解析,並且將解析後的結果存到configuration的裏面。
2.只存在url屬性的節點:和第1種很像,只是resource是項目內的路徑,url是項目外部的路徑,其實作用一樣。
3.只存在class屬性的節點:這種使用的是Java配置,採用註解的形式,它不需要將配置解析爲XNode對象,而是拿到它的接口信息,再通過configuration.addMapper()方法直接加到configuration裏面。

下面是XMLMapperBuilder的處理細節,由於代碼太長,這裏就不一一細說

public class XMLMapperBuilder extends BaseBuilder {
  //Xml解析器
  private final XPathParser parser;
  //Mapper輔助建造器
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  //資源路徑
  private final String resource;

  public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }
  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }
 private void configurationElement(XNode context) {
    try {
      //解析namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析select|insert|update|delete節點
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
}

mappers裏面的mapper節點對應的是一個mapper.xml文件,XMLMapperBuilder是專門對它進行解析的,XMLMapperBuilder通過很多步驟之後,拿到context節點,它也是mapper.xml文件的根節點mapper,然後把它傳給configurationElement()方法去處理,從中拿到"select、insert、update、delete"四種節點(如下圖)再傳給buildStatementFromContext()方法,最後通過XMLStatementBuilder類的parseStatementNode()方法將它們轉成MappedStatement並添加到configuration的mappedStatements緩存
在這裏插入圖片描述

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    String parameterType = context.getStringAttribute("parameterType");
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
   。。。。。。
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

上面是parseStatementNode()方法的實現(省略了大部分代碼),它從context節點裏面拿到id,sql腳本等信息,然後再調用builderAssistant.addMappedStatement()方法。(Mybatis的builder的類有點多, BuilderAssistant也是繼承了BaseBuilder類的,內部有configuration的成員變量,這裏最終會調用configuration的addMappedStatement()方法)

結語

好吧,Mybatis的初始化過程已經講完了,它的作用其實就是爲了初始化configuration,然後把它放到SqlSessionFactory裏面,雖然初始過程的代碼很複雜,但是Mybatis的總體的結構還是很清晰的。
這裏只講了第最開始部分,下一篇繼續介紹SqlSession管理

謝謝能夠看到這裏的各位小夥伴,本人能力有限,如果有什麼說錯的地方請指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章