mybatis源碼分析2 - SqlSessionFactory的創建

1 主要類

初始化mybatis的過程,其實就是創建SqlSessionFactory單例的過程。下面是一個簡單的初始化例子。

String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • 1
  • 2
  • 3

初始化流程大致分爲如下幾步

  1. mybatis讀取全局xml配置文件,解析XML中各個節點元素
  2. 將節點元素鍵值對,設置到Configuration實例的相關變量中
  3. 由Configuration實例創建SqlSessionFactory單例對象

我們先來分析初始化過程中涉及的主要類

1.SqlSessionFactoryBuilder:用來創建SqlSessionFactory實例,典型的builder鏈式創建模式。 
2.XMLConfigBuilder:主要有三個作用:

- 解析XML文件,生成XNode對象。XNodeXML文件中元素節點的描述。
- 創建並初始化Configuration實例
- 利用root根XNode對象,解析其各個子節點xNode,獲取其中的屬性或子元素,生成鍵值對。然後設置到Configuration實例的相關變量中
  • 1
  • 2
  • 3

3.Configuration:它是一個數據類,包含了幾乎所有的mybatis配置信息,他們會影響mybatis的執行流程。初始化階段最重要的就是創建並設置Configuration的相關變量。 
4. SqlSessionFactory:創建SqlSession實例的工廠類,一般我們都把它作爲單例使用。它的默認實現爲DefaultSqlSessionFactory

2 流程

我們一般通過new SqlSessionFactoryBuilder().build()來創建SqlSessionFactory對象。下面是SqlSessionFactoryBuilder的build()方法

// 創建SqlSessionFactory單例
// @Param inputStream: 讀取的全局XML配置文件的輸入流,配置信息都在這個文件中
// @param environment:指定的此SqlSessionFactory的數據庫環境,默認爲default
// @param properties: 設置一些動態化常量,會和XML中的properties 中常量合併在一起
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 先構建XML文件解析器
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 解析得到Configuration對象,這個對象包含了幾乎所有的mybatis配置文件中的信息,十分關鍵。
    // 然後利用Configuration對象構建SqlSessionFactory單例
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

build方法比較簡單,從中可以分析出mybatis初始化的三大流程,即讀取XML文件,設置Configuration中相關變量,創建SqlSessionFactory.下面我們一步步分析。

2.1 讀取XML文件,解析節點元素

mybatis初始化第一步,就是讀取全局XML配置文件,解析文件節點,生成xNode對象。下面來分析這個過程,先來看XMLConfigBuilder的實例化過程。

// new XMLConfigBuilder()的過程。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    // 利用XML配置文件輸入流,創建XPathParser解析對象
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  // 先創建Configuration實例對象,也會做一些基本的初始化。主要是註冊一些別名供後面解析XML用,如JDBC
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  // 將new SqlSessionFactoryBuilder().build()中傳入的Properties鍵值對,設置到configuration對象的variables變量中,它會和後面解析properties 子節點得到的鍵值對做合併。他們主要作用是配置常量動態化。
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

實例化XMLConfigBuilder後,configuration對象就創建好了,它的成員變量暫時還沒有設置好。接下來就是讀取XML文件,生成xNode節點了。xNode鏈式節點是一個XML文件的Java描述,每個xNode都對應XML中的一個節點元素。下面來分析parser.parse(),代碼如下

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 利用parser,創建xNode對象
  // parseConfiguration將解析後的鍵值對設置到Configuration實例的相關變量中,下一節詳細講解
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

解析XML文件,生成xNode鏈式節點的過程就在parser.evalNode(“/configuration”)中。它通過Apache的XPath解析器,讀取並解析各個節點。這一部分我們不用仔細分析,只需要知道XNode對象中包含了XML的配置信息即可。

2.2 設置Configuration實例的相關變量

讀取並解析了XML文件後,接下來就是將解析的XML配置信息,設置到configuration實例的相關變量中了。接着上一節的parseConfiguration()方法講。

// 解析由XML文件生成的XNode對象,生成Configuration對象。這個是關鍵方法
private void parseConfiguration(XNode root) {
  try {
    // 解析properties  節點,存放到Configuration對象的variables變量中,用來將配置變量動態化。
    // 如配置dataSource的username password
    propertiesElement(root.evalNode("properties"));

    // 解析settings  節點,會改寫Configuration中的相關值。
    // 這些值決定了mybatis的運行方式,如CacheEnabled lazyLoadingEnabled等屬性
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);

    // 解析typeAliases  節點,定義別名,一般用來爲Java全路徑類型取一個比較短的名字
    typeAliasesElement(root.evalNode("typeAliases"));

    // 解析plugins  節點, 定義插件,用來攔截某些類,從而改變這些類的執行
    // 如四大基本插件,Executor ParameterHandler StatementHandler ResultSetHandler
    pluginElement(root.evalNode("plugins"));

    // 解析objectFactory  節點,定義對象工廠,不常用。對象工廠用來創建mybatis返回的結果對象
    objectFactoryElement(root.evalNode("objectFactory"));

    // 解析objectWrapperFactory  節點, 包裝Object實例,不常用
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

    // 解析reflectorFactory  節點,創建Reflector類反射,不常用
    reflectorFactoryElement(root.evalNode("reflectorFactory"));

    // 將解析並讀取settings 節點後得到的鍵值對,設置到Configuration實例的相關變量中
    // 這些鍵值對決定了mybatis的運行方式,如果沒設置,則採用默認值
    settingsElement(settings);

    // 解析environments  節點, 定義數據庫環境。
    // 可以配置多個environment,每個對應一個dataSource和transactionManager
    environmentsElement(root.evalNode("environments"));

    // 解析databaseIdProvider  節點,定義數據庫廠商標識。
    // mybatis可以根據不同的廠商執行不同的語句
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));

    // 解析typeHandlers  節點, 定義類型處理器,用來將數據庫中獲取的值轉換爲Java類型
    typeHandlerElement(root.evalNode("typeHandlers"));

    // 解析mappers  節點, 定義映射器,也就是SQL映射語句。mappers中定義好映射文件的位置即可。
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

parseConfiguration逐一讀取XML配置信息的子節點,然後將它們設置到configuration變量中,比如properties,typeAliases,mappers等。下面我們逐一分析。先看properties節點

// 讀取並解析properties 節點, 合併到configuration的variables變量中
private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 獲取properties 中的resource或者url屬性,二者必須定義一個,否則定義有誤
    Properties defaults = context.getChildrenAsProperties();
    String resource = context.getStringAttribute("resource");
    String url = context.getStringAttribute("url");
    if (resource != null && url != null) {
      throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
    }

    // 從resource或者url中讀取資源,生成Properties對象。Properties是一個 Hashtable,保存了<key,value>的鍵值對
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }

    // 將configuration本來就存在的Hashtable,variables變量,和從resource或者url中加載的Hashtable合併,更新到variables變量中。
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    configuration.setVariables(defaults);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

propertiesElement用來解析properties節點,合併到configuration的variables變量中。variables變量是由new SqlSessionFactoryBuilder().build(Reader reader, String environment, Properties properties)時傳入的初始properties初始化的,詳細可參見前面的分析。下面來分析settings 節點。

// 讀取並解析settings元素,生成Properties鍵值對
private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  Properties props = context.getChildrenAsProperties();
  // Check that all settings are known to the configuration class
  MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  for (Object key : props.keySet()) {
    if (!metaConfig.hasSetter(String.valueOf(key))) {
      throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
    }
  }
  return props;
}

// 獲取用戶自定義的vfs的實現,配置在settings元素中。
// settings中放置自定義vfs實現類的全限定名,以逗號分隔
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
  String value = props.getProperty("vfsImpl");
  if (value != null) {
    // vfs實現類可以有多個,其實現類全限定名以逗號分隔
    String[] clazzes = value.split(",");
    for (String clazz : clazzes) {
      if (!clazz.isEmpty()) {
        // 反射加載自定義vfs實現類,並設置到Configuration實例中
        Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
        configuration.setVfsImpl(vfsImpl);
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

這一步還沒有將settings配置信息設置到configuration中,會通過後面的settingsElement(settings)方法進行設置。之所以放在後面,是因爲解析settings時,會利用到其他一些節點元素的解析結果,比如typeAliases的別名,plugin的攔截器等。settingsElement()源碼如下

// 將解析並讀取settings節點後得到的鍵值對,設置到Configuration實例的相關變量中
private void settingsElement(Properties props) throws Exception {
  // 設置configuration的autoMappingBehavior變量, 指定mybatis應如何自動映射數據庫列到POJO對象屬性。
  // 爲NONE表示取消自動映射,PARTIAL表示只映射非嵌套結果集,FULL表示映射所有結果集,即使有嵌套。
  configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
  // 設置autoMappingUnknownColumnBehavior,mybatis自動映射時對未定義列應如何處理。
  // 爲NONE則不做任何處理,WARNING輸出提醒日誌,FAILING映射失敗,拋出SqlSessionException
  configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
  // 設置cacheEnabled, 是否開啓二級緩存,也就是SqlSessionFactory級別的緩存。一級緩存,也即sqlSession內的HashMap,是默認開啓的。
  configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
  // 設置proxyFactory,指定mybatis創建延遲加載對象所用到的動態代理工具。可爲CGLIB 或 JAVASSIST
  configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
  // 設置lazyLoadingEnabled,延遲加載,開啓時所有關聯對象都會延遲加載,除非關聯對象中設置了fetchType來覆蓋它。
  configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
  // 設置aggressiveLazyLoading,開啓時,調用對象內任何方法都會加載對象內所有屬性,默認爲false,即按需加載
  configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
  // 設置 multipleResultSetsEnabled,開啓時,允許單一語句返回多結果
  configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
  // 設置useColumnLabel,使用列標籤代替列名
  configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
  // 設置useGeneratedKeys,允許JDBC自動生成主鍵
  configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
  // 設置defaultExecutorType,默認執行器
  // SIMPLE 普通執行器;REUSE 重用預處理語句;BATCH 批量更新
  configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
  // 設置defaultStatementTimeout,超時時間,也就是數據庫驅動等待數據庫響應的時間,單位爲秒
  configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
  // 設置defaultFetchSize,每次返回的數據庫結果集的行數。使用它可避免內存溢出
  configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
  // 設置mapUnderscoreToCamelCase,開啓自動駝峯命名規則映射,即將數據庫列名a_column映射爲Java屬性aColumn
  configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
  // 設置safeRowBoundsEnabled,允許在嵌套語句中使用分頁RowBounds
  configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
  // 設置localCacheScope,本地緩存的作用域。本地緩存用來加速嵌套查詢和防止循環引用
  // 爲SESSION,則緩存本sqlSession中的所有查詢語句。爲STATEMENT,則相同sqlSession的同一個調用語句才做緩存。
  configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
  // 設置jdbcTypeForNull,沒有爲參數提供特定的JDBC類型時,Java NULL對應的JDBC類型
  // 可爲 NULL、VARCHAR 或 OTHER
  configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
  // 設置lazyLoadTriggerMethods,指定哪些方法會觸發延遲加載關聯對象。方法名之間用逗號隔開,如equals,clone,hashCode,toString
  configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
  // 設置safeResultHandlerEnabled,允許在嵌套語句中使用分頁(ResultHandler)
  configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
  // 設置defaultScriptingLanguage,指定用來生成動態SQL語句的默認語言
  configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
  // 設置defaultEnumTypeHandler,指定Enum對應的默認的TypeHandler. 它是一個Java全限定名。如org.apache.ibatis.type.EnumTypeHandler
  @SuppressWarnings("unchecked")
  Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
  configuration.setDefaultEnumTypeHandler(typeHandler);
  // 設置callSettersOnNulls,指定結果集中null值是否調用Java映射對象的setter
  configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
  // 設置useActualParamName,允許使用方法簽名中的形參名作爲SQL語句的參數名稱。使用這個屬性時,工程必須支持Java8
  configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
  // 設置returnInstanceForEmptyRow,當某一行的所有列都爲空時,mybatis返回一個null實例對象。
  configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
  // 設置logPrefix,指定mybatis添加到日誌中的前綴。設置後每一條日誌都會添加這個前綴
  configuration.setLogPrefix(props.getProperty("logPrefix"));
  // 設置logImpl,指定mybatis使用的日誌系統,如LOG4J
  @SuppressWarnings("unchecked")
  Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
  configuration.setLogImpl(logImpl);
  // 設置configurationFactory,指定生成Configuration對象的工廠類。工廠類必須包含getConfiguration()方法
  configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

分析完settings節點後,我們來分析typeAliases節點,源碼如下

// 讀取並解析typeAliases元素,並設置到Configuration的typeAliasRegistry中
private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 元素爲package時,mybatis會在搜索包名下需要的Java bean。使用bean的首字母的小寫的非限定名來作爲它的別名
        String typeAliasPackage = child.getStringAttribute("name");
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        // 元素爲typeAlias時,讀取單個typeAlias 的alias和type元素,它們分別是別名和原名
        String alias = child.getStringAttribute("alias");   // 別名
        String type = child.getStringAttribute("type");     // 原名
        try {
          // 反射加載type對應的原類型,然後以alias作爲key,class對象作爲value放入TYPE_ALIASES這個Map中。
          // 這樣使用到別名的時候就可以使用真實的class類來替換
          // 這個Map定義了很多默認的別名映射,如string byte int
          Class<?> clazz = Resources.classForName(type);
          if (alias == null) {
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

typeAliasesElement會讀取並解析typeAliases元素,並設置到Configuration的typeAliasRegistry中。它的子元素可以爲package 或者typeAlias。下面分析plugins 節點

// 讀取並解析plugin元素,並添加到Configuration的interceptorChain中
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍歷plugins中的每個plugin
    for (XNode child : parent.getChildren()) {
      // 讀取plugin中的interceptor屬性,它聲明瞭插件的實現類的全限定名
      String interceptor = child.getStringAttribute("interceptor");
      // 讀取plugin中的property元素,它聲明瞭插件類的構造參數。
      Properties properties = child.getChildrenAsProperties();
      // 有了實現類的全限定名和構造參數後,就可以反射創建插件對象實例了,並初始化它
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      // 將創建並初始化好的插件對象實例添加到Configuration對象中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

pluginElement會讀取並解析plugin元素,並添加到Configuration的interceptorChain中。讀取它的plugin子節點,解析其中的interceptor屬性和property子元素,來反射創建並初始化插件實例。下面來分析environments 節點。

// 讀取並解析environments元素,並設置到Configuration實例中
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    // environment爲SqlSessionFactoryBuilder().build(inputStream,environment)時傳入的String,
    // 它指定了SqlSessionFactory所使用的數據庫環境。不聲明的話則採用XML中default元素對應的數據庫環境
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }

    // 遍歷各個子environment
    for (XNode child : context.getChildren()) {
      // 先獲取environment的id元素
      String id = child.getStringAttribute("id");

      // 判斷id是否等於上面指定的environment String。因爲我們只需要加載我們指定的environment即可。
      // 這兒就可以明白爲啥XML中可以配置多個數據庫環境,運行時可以由我們動態選擇了。
      if (isSpecifiedEnvironment(id)) {

        // 獲取transactionManager元素,創建TransactionFactory實例並初始化它。
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

        // 獲取dataSource元素, 創建DataSourceFactory實例並初始化它
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();

        // builder設計模式創建Environment對象,它包含id,transactionFactory,dataSource成員變量
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);

        // 將創建好的Environment對象設置到configuration實例中
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

environmentsElement()讀取並解析environments元素,並設置到Configuration實例中。他會根據new SqlSessionFactoryBuilder().build(Reader reader, String environment, Properties properties)時指定的數據庫環境environment,來找到對應的environments 子節點,然後讀取它的transactionManager 和dataSource 子節點,創建並初始化它們,然後設置到Configuration實例的environment變量中。下面來看typeHandlers 子節點。

// 讀取並解析typeHandler元素,並添加到typeHandlerRegistry變量中
private void typeHandlerElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 使用package子元素時,mybatis自動檢索包名下添加了相關注解的typeHandler,實例化它
        String typeHandlerPackage = child.getStringAttribute("name");
        typeHandlerRegistry.register(typeHandlerPackage);
      } else {
        // 使用typeHandler子元素時,分別讀取javaType, jdbcType, handler三個元素。
        // javaType對應Java類型 jdbcType對應數據庫中的類型,handler則爲類處理器
        // 比如我們將varchar轉變爲String,就需要一個typeHandler
        String javaTypeName = child.getStringAttribute("javaType");
        String jdbcTypeName = child.getStringAttribute("jdbcType");
        String handlerTypeName = child.getStringAttribute("handler");

        // 利用javaTypeName, jdbcTypeName, handlerName創建實例化對象,可以爲別名或者全限定名。
        // typeAlias這個時候就派上用場了。
        Class<?> javaTypeClass = resolveClass(javaTypeName);
        JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
        Class<?> typeHandlerClass = resolveClass(handlerTypeName);

        // 將實例化好的對象添加到typeHandlerRegistry中,mybatis運行時就需要用到它了。此時是註冊階段。
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

typeHandlerElement() 讀取並解析typeHandler元素,並添加到typeHandlerRegistry變量中。它的子元素可以爲package 或typeHandler 。根據javaType jdbcType handler 三個屬性指定的全限定類名或別名,創建並初始化javaTypeClass JdbcType typeHandlerClass,然後設置到typeHandlerRegistry變量中。下面來分析mappers節點。mappers定義了SQL語句映射關係,要複雜很多。

// 讀取並解析mappers元素,並添加到configuration實例的mapperRegistry變量中
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 子元素爲package時,mybatis將包名下所有的接口認爲是mapper類。創建其類對象並添加到mapperRegistry中。
        // 此時一般是註解方式,不需要使用XML mapper文件。
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);

      } else {
        // 子元素爲mapper時,讀取子元素的resource或url或class屬性。
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");

        if (resource != null && url == null && mapperClass == null) {
          // resource屬性不爲空時,讀取resource對應的XML資源,並解析它
          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) {
          // url屬性不爲空時,讀取url對應的xml資源,並解析它
          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屬性不爲空時,直接創建class對應的類對象,並添加到configuration中。
          // 僅使用mapperClass,而不使用XML mapper文件,一般是註解方式。我們一般不建議採用註解方式。
          // 後面會詳細分析註解方式
          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.");
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

mapperElement會讀取並解析mappers元素,並添加到configuration實例的mapperRegistry變量中。子元素可以爲package 或mapper 。我們知道,mapper配置可以採用XML方式或者註解方式。註解方式對應package子元素或mapper子元素下的class方式。XML方式對應mapper子元素下的resource或url方式。先來看註解方式,從上面的configuration.addMapper(mapperInterface);看起

// configuration.addMapper()使用代理方式,調用到mapperRegistry中。
// configuration包含了幾乎所有的配置信息,是配置的門面,十分複雜。
// 故採用外觀模式和代理模式,將真正實現下沉到各個子系統中。這樣通過分層可以解耦和降低複雜度。
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
 }

// 添加mapperClass到map中保存起來,並解析它的註解
public <T> void addMapper(Class<T> type) {
  // 我們定義的mybatis的mapper類必須是一個接口
  if (type.isInterface()) {
    // 已經添加過了的mapper就不再添加了
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }

    // 防止併發重入
    boolean loadCompleted = false;
    try {
      // 將新解析的mapper接口類添加到map中
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      // 解析Mapper接口的各項註解,比如@Select,這是最關鍵的地方
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

// 遍歷Mapper接口類的每個方法,解析其註解,生成MappedStatement,SqlSource和BoundSql三大主要對象
public void parse() {
    // type爲mapper接口類
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 根據mapper接口類名,獲取XML資源文件,並加載。Spring-mybatis會使用到,僅使用mybatis時貌似不會用到
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      // 解析CacheNamespace註解,Mapper接口類註解
      parseCache();
      // 解析CacheNamespaceRef註解,Mapper接口類註解
      parseCacheRef();

      // 遍歷mapper接口的各個方法,然後解析方法上的註解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // 解析mapper,此處是關鍵
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

註解方式下,解析Mapper接口類時,我們先將Mapper接口添加到Map中保存起來,然後解析類註解@CacheNamespace @CacheNamespaceRef。之後就開始最關鍵的步驟,遍歷Mapper的各個方法,解析方法上的註解。下面我們接着分析。

void parseStatement(Method method) {
  // 反射獲取入參類型和@Lang註解,爲構建SqlSource做準備
  Class<?> parameterTypeClass = getParameterType(method);
  LanguageDriver languageDriver = getLanguageDriver(method);
  // 這一步十分關鍵,是創建SqlSource和其內的BoundSql對象的關鍵所在。
  // SqlSource是MappedStatement的一個屬性,並用來提供BoundSql對象
  // BoundSql用來建立sql語句,它包含sql String,入參parameterObject和入參映射parameterMappings。它利用sql語句和入參,組裝成最終的訪問數據庫的SQL語句,包括動態SQL。這是mybatis Mapper映射的最核心的地方。
  // 獲取@Select @Insert @Update @Delete等註解,或者@SelectProvider @InsertProvider @UpdateProvider @DeleteProvider等註解
  // 然後解析這些註解,利用註解的value,也就是sql String解析生成SqlSource對象。後面詳細分析
  SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

  // 後面代碼比較多,大多是解析方法上其他註解,如@Options @SelectKey @ResultMap。這裏省略
  if (sqlSource != null) {
    ...
  }
  ...

  // 利用上面解析得到的sqlSource等變量構建mappedStatement
  assistant.addMappedStatement(...);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

下面來看sqlSource和BoundSql是如何創建的。

// 根據mapper的方法上的註解,解析得到sqlSource對象。
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
  try {
    // 解析獲取 @Select @Insert @Delete @Update四者中的一個
    Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
    // 解析獲取 @SelectProvider @InsertProvider @DeleteProvider @UpdateProvider四者中的一個
    Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
    if (sqlAnnotationType != null) {
      if (sqlProviderAnnotationType != null) {
        // 靜態SQL註解和sqlProvider註解不可能同時存在
        throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
      }
      // 反射獲取靜態sql註解
      Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
      // 獲取註解中的value,也就是sql String
      final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
      // 利用sql String和入參來組裝SqlSource對象,由這兩者就可以生成最終訪問數據庫的SQL語句了
      // 後面詳細分析
      return buildSqlSourceFromStrings(strings, parameterType, languageDriver);

    } else if (sqlProviderAnnotationType != null) {
      // 反射獲取SQLProvider註解,構建ProviderSqlSource類型的SqlSource。這一部分我們就不詳細展開了,讀者可自行分析
      Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
      return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
    }
    return null;
  } catch (Exception e) {
    throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
  }
}

// 利用sql String和入參來組裝SqlSource對象,由這兩者就可以生成最終訪問數據庫的SQL語句了
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    // 將sql String 拼裝起來,構成一整個sql。此時sql中還沒有注入入參
    final StringBuilder sql = new StringBuilder();
    for (String fragment : strings) {
      sql.append(fragment);
      sql.append(" ");
    }
    // 構建sqlSource,由具體LanguageDriver實現類完成,如XMLLanguageDriver。後面不詳細分析了
    return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

2.3 由Configuration實例創建SqlSessionFactory單例對象

讀取XNode中的XML配置信息,並設置到Configuration實例的相關變量中後,就可以由Configuration實例創建SqlSessionFactory單例對象了。這個過程比較簡單,源碼如下。

// 利用Configuration實例創建DefaultSqlSessionFactory對象
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

// 默認的SqlSessionFactory實現類。
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

創建SqlSessionFactory對象的過程十分簡單,利用Configuration實例構造DefaultSqlSessionFactory對象即可。DefaultSqlSessionFactory是SqlSessionFactory接口的默認實現類。

3 總結

SqlSessionFactory的創建,其實也就是mybatis的初始化過程。其中的重中之重就是Configuration實例的相關變量的設置。mybatis運行時會讀取這些變量,來決定執行流程。這也是mybatis設計的精巧之處,代碼配置化將設置與運行相分離,配置信息XML化也大大降低了Java代碼複雜度,提高了可讀性和可維護性。這些精巧的構思都是我們設計框架時可以吸取的寶貴經驗。

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