【MyBatis原始碼分析】Configuration載入(下篇)

五月的倉頡發表於2017-05-08

元素設定

繼續MyBatis的Configuration載入原始碼分析:

 1 private void parseConfiguration(XNode root) {
 2     try {
 3       Properties settings = settingsAsPropertiess(root.evalNode("settings"));
 4       //issue #117 read properties first
 5       propertiesElement(root.evalNode("properties"));
 6       loadCustomVfs(settings);
 7       typeAliasesElement(root.evalNode("typeAliases"));
 8       pluginElement(root.evalNode("plugins"));
 9       objectFactoryElement(root.evalNode("objectFactory"));
10       objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
11       reflectorFactoryElement(root.evalNode("reflectorFactory"));
12       settingsElement(settings);
13       // read it after objectFactory and objectWrapperFactory issue #631
14       environmentsElement(root.evalNode("environments"));
15       databaseIdProviderElement(root.evalNode("databaseIdProvider"));
16       typeHandlerElement(root.evalNode("typeHandlers"));
17       mapperElement(root.evalNode("mappers"));
18     } catch (Exception e) {
19       throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
20     }
21 }

上回看到了第7行的<typeAlias>標籤的解析,後面先暫時跳過<plugins>、<objectFactory>、<objectWrapperFactory>、<reflectorFactory>、<typeHandlers>、<databaseIdProvider>這幾部分,這幾部分要麼屬於MyBatis中不太常用的,要麼屬於MyBatis中比較進階的應用,之後再說。

現在先看一下元素設定的程式碼,即第12行的settingsElement方法:

 1 private void settingsElement(Properties props) throws Exception {
 2     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
 3     configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
 4     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
 5     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
 6     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
 7     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
 8     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
 9     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
10     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
11     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
12     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
13     configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
14     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
15     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
16     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
17     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
18     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
19     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
20     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
21     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
22     configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), false));
23     configuration.setLogPrefix(props.getProperty("logPrefix"));
24     @SuppressWarnings("unchecked")
25     Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
26     configuration.setLogImpl(logImpl);
27     configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
28 }

看到這個方法的實現主要就是將之前解析出來的<settings>中的內容設定到Configuration中。好像一直忘了說一個事,Configuration是XMLConfigBuilder的父類BaseBuilder中的一個屬性,BaseBuilder中儲存了三個重要屬性,畫一張圖來表示一下:

 

environments載入

接著就是<environments>的載入了,一個比較重要的屬性,用於配置JDBC資訊,對應的是environmentsElement(root.evalNode("environments"))這句程式碼:

 1 private void environmentsElement(XNode context) throws Exception {
 2     if (context != null) {
 3       if (environment == null) {
 4         environment = context.getStringAttribute("default");
 5       }
 6       for (XNode child : context.getChildren()) {
 7         String id = child.getStringAttribute("id");
 8         if (isSpecifiedEnvironment(id)) {
 9           TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
10           DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
11           DataSource dataSource = dsFactory.getDataSource();
12           Environment.Builder environmentBuilder = new Environment.Builder(id)
13               .transactionFactory(txFactory)
14               .dataSource(dataSource);
15           configuration.setEnvironment(environmentBuilder.build());
16         }
17       }
18     }
19 }

第3行~第5行的程式碼,得到預設的JDBC環境名稱。

第6行的程式碼開始遍歷<environments>標籤下的每一個<environment>標籤,先第7行的程式碼獲取<environment>下的id屬性,接著第8行的程式碼判斷當前的<environment>是不是預設的JDBC環境,也就是第3行~第5行程式碼獲取到的default屬性對應的值。從這段程式碼可以看出兩個問題:

  1. 原始碼並沒有對不滿足第8行判斷即不是預設<environment>的場景做判斷,因此可以得到一個結論:<environments>標籤下的default屬性是一個必填屬性
  2. 即使配置再多的<environment>標籤,MyBatis只會載入其中的一個<environment>

第9行的程式碼根據<transactionManager>標籤獲取事物管理器,本系列文章配置的是"JDBC",那麼例項化出來的是JdbcTransactionFactory(JDBC-->JdbcTransactionFactory的對應關係在Configuration建構函式配置的alias對映中),其他的還有ManagedTransactionFactory和SpringManagedTransactionFactory,其中前者是MyBatis原生支援的,後者是Spring框架支援的。

第10行的程式碼和第9行的程式碼差不多,根據<dataSource>標籤獲取資料來源工廠DataSourceFactory,本系列文章配置的是"POOLED",那麼例項化出來的是PooledDataSourceFactory(POOLED-->PooledDataSourceFactory的對應關係在Configuration建構函式配置的alias對映中),其他的還有UnpooledDataSourceFactory和JndiDataSourceFactory。

第11行的程式碼根據DataSourceFactory獲取DataSource,在MyBatis中根據配置分三種場景:

  • PooledDataSourceFactory對應的DataSource是PooledDataSource
  • UnpooledDataSourceFactory對應的DataSource是UnpooledDataSource
  • JndiDataSourceFactory對應的DataSource要去JNDI服務上去找

第12行~第15行的程式碼比較簡單,根據TransactionFactory和DataSource建立一個Environment並設定到Configuration。

 

mapper載入

config.xml中兩個最重要的標籤,一個是<environment>(JDBC環境資訊),另一個就是mapper(sql檔案對映)了。mapper的載入是"mapperElement(root.evalNode("mappers"))"這句程式碼,看一下實現:

 1 private void mapperElement(XNode parent) throws Exception {
 2     if (parent != null) {
 3       for (XNode child : parent.getChildren()) {
 4         if ("package".equals(child.getName())) {
 5           String mapperPackage = child.getStringAttribute("name");
 6           configuration.addMappers(mapperPackage);
 7         } else {
 8           String resource = child.getStringAttribute("resource");
 9           String url = child.getStringAttribute("url");
10           String mapperClass = child.getStringAttribute("class");
11           if (resource != null && url == null && mapperClass == null) {
12             ErrorContext.instance().resource(resource);
13             InputStream inputStream = Resources.getResourceAsStream(resource);
14             XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
15             mapperParser.parse();
16           } else if (resource == null && url != null && mapperClass == null) {
17             ErrorContext.instance().resource(url);
18             InputStream inputStream = Resources.getUrlAsStream(url);
19             XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
20             mapperParser.parse();
21           } else if (resource == null && url == null && mapperClass != null) {
22             Class<?> mapperInterface = Resources.classForName(mapperClass);
23             configuration.addMapper(mapperInterface);
24           } else {
25             throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
26           }
27         }
28       }
29     }
30 }

看到<mappers>下可以定義<mapper>和<package>兩種子標籤,它們同樣是二選一的關係,即只能定義其中一種,這裡先看package分支的內容即根據類路徑載入Mapper就不看了,基本不用的,就看else分支裡面的內容,即根據<mapper>標籤解析sql對映。

接著第8行~第10行分別獲取每一個<mapper>中的resource、url、mapperClass,接著下面的判斷很有意思:

  • resource != null && url == null && mapperClass == null
  • resource == null && url != null && mapperClass == null
  • resource == null && url == null && mapperClass != null

這告訴我們了resource、url、mapperClass三個屬性只能定義其中的一個,else分支中丟擲的異常同樣也印證了這一說法。本系列文章的例子定義的是resource且定義resource的方式最常用,因此進入第一個if判斷。

第12行的程式碼上下文設定一下resource,不是很重要。

第13行的程式碼根據mapper檔案路徑獲取InputStream,InputStream在之後將會被轉為InputSource用來解析mapper檔案。

第14行的程式碼獲取一個XMLMapperBuilder,它的流程和上文分析的XMLConfigBuilder是一樣的,裡面也使用的是XPathParser將mapper檔案解析為Document。

第15行的程式碼跟進去看一下實現,因為XMLMapperBuilder的parse方法和XMLConfigBuilder的parse方法有區別,畢竟解析的是兩種MyBatis配置檔案:

 1 public void parse() {
 2     if (!configuration.isResourceLoaded(resource)) {
 3       configurationElement(parser.evalNode("/mapper"));
 4       configuration.addLoadedResource(resource);
 5       bindMapperForNamespace();
 6     }
 7 
 8     parsePendingResultMaps();
 9     parsePendingChacheRefs();
10     parsePendingStatements();
11 }

第2行的程式碼判斷了當前資源是否被載入過,如果沒有被載入過則會執行第3行~第5行的程式碼。

首先是第3行的程式碼configurationElement:

 1 private void configurationElement(XNode context) {
 2     try {
 3       String namespace = context.getStringAttribute("namespace");
 4       if (namespace == null || namespace.equals("")) {
 5         throw new BuilderException("Mapper's namespace cannot be empty");
 6       }
 7       builderAssistant.setCurrentNamespace(namespace);
 8       cacheRefElement(context.evalNode("cache-ref"));
 9       cacheElement(context.evalNode("cache"));
10       parameterMapElement(context.evalNodes("/mapper/parameterMap"));
11       resultMapElements(context.evalNodes("/mapper/resultMap"));
12       sqlElement(context.evalNodes("/mapper/sql"));
13       buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
14     } catch (Exception e) {
15       throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
16     }
17 }

第3行的程式碼獲取當前mapper檔案的namespace,namespace是一個很重要的屬性,所有的<sql>、<resultMap>、<insert>、<delete>、<update>、<select>標籤,它們的id都是和namespace繫結的,從而確保全域性的唯一性,當namespace未定義或者為空字串的時候,第5行就會丟擲異常,因此每個mapper檔案的namespace都是一個必填內容

第7行的程式碼在MapperBuilderAssistant中設定了一下namespace,這樣後文可以通過MapperBuilderAssistant拿namespace而不需要每次傳一個String型別的引數。

第8行~第13行的程式碼分別用於解析<cache-ref>、<cache>、<parameterMap>、<resultMap>、<sql>、<select>、<insert>、<update>、<delete>這幾個標籤,逐個看一下:

  • cacheRefElement方法用於解析<cache-ref>標籤,總結如下:
  1. 解析完的CacheRef放在cacheRefMap中
  2. cacheRefMap是一個HashMap
  3. 位於Configuration物件中
  4. Key為mapper檔案的namespace,Value為<cache-ref>中配置的namespace
  • cacheElement方法用於解析<cache>標籤,總結如下:
  1. 會根據<cache>中配置的屬性new出一個org.apache.ibatis.cache.Cache
  2. 使用此Cache作為MyBatis快取
  • parameterMapElement方法用於解析<parameterMap>標籤,總結如下:
  1. 解析完的ParameterMap放在parameterMaps中
  2. parameterMaps是一個StrictMap
  3. 位於Configuration物件中,StrictMap是HashMap的子類
  4. Key為當前mapper的namespace+"."+<parameterMap>標籤中的id屬性,Value為ParameterMap物件
  • resultMapElements方法用於解析<resultMap>標籤在,總結如下:
  1. 解析完的ResultMap放在resultMaps中
  2. resultMaps是一個StrictMap,
  3. 位於Configuration物件中
  4. Key為當前mapper的namespace+"."+<resultMap>標籤中的id屬性,Value為ResultMap物件
  • sqlElement方法用於解析<sql>標籤,總結如下:
  1. 解析完的內容放在sqlFragments中
  2. sqlFragments是一個StrictMap
  3. 位於XMLMapperBuilder物件中
  4. Key為當前mapper的namespace+"."+<sql>標籤中的id屬性Value為sql這個XNode本身
  • buildStatementFromContext用於解析<select>、<insert>、<update>、<delete>這四個標籤,總結如下:
  1. 解析完的內容放在mappedStatements中
  2. mappedStatements是一個StrictMap
  3. 位於Configuration物件中
  4. Key為當前mapper的namespace+"."+<select>|<insert>|<update>|<delete>標籤中的id屬性Value為MappedStatement物件

 

構建SqlSessionFactory

最後一步,構建SqlSessionFactory,回看前面SqlSessionFactoryBuilder的build方法:

 1 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
 2     try {
 3       XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
 4       return build(parser.parse());
 5     } catch (Exception e) {
 6       throw ExceptionFactory.wrapException("Error building SqlSession.", e);
 7     } finally {
 8       ErrorContext.instance().reset();
 9       try {
10         inputStream.close();
11       } catch (IOException e) {
12         // Intentionally ignore. Prefer previous error.
13       }
14     }
15 }

第4行方法的parser.parse()這句之前一直在分析,將配置檔案轉換為了MyBatis中定義的各種物件且絕大部分配置儲存在Configuration中,少部分配置儲存在XMLConfigBuilder的父類BaseBuilder中。

接著就是外層的build方法了,看下實現:

 1 public SqlSessionFactory build(Configuration config) {
 2     return new DefaultSqlSessionFactory(config);
 3 }

最終構建出來的SqlSessionFactory是DefaultSqlSessionFactory,以Configuration物件為形參。

相關文章