元素設定
繼續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屬性對應的值。從這段程式碼可以看出兩個問題:
- 原始碼並沒有對不滿足第8行判斷即不是預設<environment>的場景做判斷,因此可以得到一個結論:<environments>標籤下的default屬性是一個必填屬性。
- 即使配置再多的<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>標籤,總結如下:
- 解析完的CacheRef放在cacheRefMap中
- cacheRefMap是一個HashMap
- 位於Configuration物件中
- Key為mapper檔案的namespace,Value為<cache-ref>中配置的namespace
- cacheElement方法用於解析<cache>標籤,總結如下:
- 會根據<cache>中配置的屬性new出一個org.apache.ibatis.cache.Cache
- 使用此Cache作為MyBatis快取
- parameterMapElement方法用於解析<parameterMap>標籤,總結如下:
- 解析完的ParameterMap放在parameterMaps中
- parameterMaps是一個StrictMap
- 位於Configuration物件中,StrictMap是HashMap的子類
- Key為當前mapper的namespace+"."+<parameterMap>標籤中的id屬性,Value為ParameterMap物件
- resultMapElements方法用於解析<resultMap>標籤在,總結如下:
- 解析完的ResultMap放在resultMaps中
- resultMaps是一個StrictMap,
- 位於Configuration物件中
- Key為當前mapper的namespace+"."+<resultMap>標籤中的id屬性,Value為ResultMap物件
- sqlElement方法用於解析<sql>標籤,總結如下:
- 解析完的內容放在sqlFragments中
- sqlFragments是一個StrictMap
- 位於XMLMapperBuilder物件中
- Key為當前mapper的namespace+"."+<sql>標籤中的id屬性,Value為sql這個XNode本身
- buildStatementFromContext用於解析<select>、<insert>、<update>、<delete>這四個標籤,總結如下:
- 解析完的內容放在mappedStatements中
- mappedStatements是一個StrictMap
- 位於Configuration物件中
- 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物件為形參。