MyBatis 的前身是 Apache 的開源項目 iBatis。MyBatis 消除了幾乎全部的 JDBC 代碼和參數的手工設置以及對結果集的檢索封裝,是一個支持普通 SQL 查詢,存儲過程和高級映射的基於 Java 的優秀持久層框架。java
當閱讀源碼的時候咱們不能深陷一些細節,咱們應該先鳥瞰全貌,這樣可以幫助你從高維度理解框架。node
由於這篇文章主要涉及配置文件對應的配置對象的初始化和構建,因此執行部分先不作介紹。咱們首先放一張平時咱們使用 Mybatis 的時候會編寫的兩個重要配置文件——mybatis-config.xml 和 xxxMapper.xml。sql
這裏咱們默認 mybatis 的配置文件爲 mybatis-config.xml 數據庫
在 mybatis-config.xml 配置文件中,咱們會有一個專門的 <mappers>
標籤映射了相關的 mapper 映射文件。緩存
其實,Mybatis的構建流程就是:對配置文件解析成配置對象裝入內存中。mybatis
首先咱們來思考一個問題:這個配置對象何時會被使用到?app
咱們知道在 mybatis-config.xml 中配置了一些類型處理器,類型別名,mappers,數據庫鏈接信息等等,而這些東西在每次數據庫鏈接進行 CRUD 操做的時候都須要用到,也就是在每次 SQL會話 中咱們須要用到。框架
而在 Mybatis 中使用了一個 SqlSession
接口來表示和規範了 Sql會話,咱們須要經過專門的 SqlSessionFactory
去建立,這裏面是一種工廠模式。這裏我簡單畫一下 UML 圖,你能夠回顧一下 工廠模式,但這不是這篇文章的重點。ide
Mybatis使用了工廠模式還不止,在構造 SqlSessionFactory
的時候還使用了 SqlSessionFactoryBuilder
去構建 SqlSessionFactory
也就是使用了構建者模式。而又由於在建立 SqlSession
的時候咱們須要傳入咱們的配置對象 Configuration
,而咱們知道 mybatis-config.xml 配置文件中有許多標籤,也就意味着當咱們構造一個 Configuration
對象的時候會帶有不少字段的解析,那麼整個 Configuration
對象的構建是很是複雜的。在 Mybatis 中使用了 構建者模式 來解決這個問題。咱們能夠看一下源碼。源碼分析
// 這是在SqlSessionFactoryBuilder類中
// 在 SqlSessionFactoryBuilder 中會有不少build構造工廠的方法
// 其中這裏是主線,由於其餘build方法都會調用此方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 經過配置文件解析成的流去建立
// 構建Configuration對象的 builder類 XmlConfigBuilder
// 以後會調用parse方法構建 Configuration 對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 最終會調用參數爲Configuration的build方法
// 進行最終的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.
}
}
}
複製代碼
這樣咱們就能夠畫出一個簡單的流程圖了。
由上面的分析咱們能夠知道:在 XMLConfigBuilder 類中的 parse() 方法進行了 Configuration 對象的解析和構建。
咱們來沿着這條路線進去看看底層原理是什麼樣的。
public Configuration parse() {
// 若是已經解析過了,報錯
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 根節點是configuration
// 解析還在這裏
// 我須要在這裏解釋一下
// "/configuartion" 這個實際上是xpath語法
// mybatis封裝了 xpath解析器去解析 xml
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步驟解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.類型別名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.對象工廠
objectFactoryElement(root.evalNode("objectFactory"));
//5.對象包裝工廠
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.設置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.環境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.類型處理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
複製代碼
看到這些有沒有以爲很熟悉?
其實就是 配置文件中的一些標籤配置。咱們畫張圖來對應一下就一目瞭然了。
如上圖所示,在整個 Configuration
配置對象的構建過程當中須要涉及到不少標籤的解析,因此 Mybatis 巧妙地利用了 構建者模式,而這麼多配置信息在這篇文章中我不能一一去進行源碼分析(有不少都是細枝末節的東西,咱們只須要大概知道幹什麼就好了),因此我挑了最重要的 <mappers>
標籤的解析去進行源碼分析。
咱們再次進入源碼查看,這回是 XmlConfigBuilder
中的 mapperElement(XNode parent)
方法。固然咱們最好對照着配置信息格式去看
<mappers>
<!-- 這幾種配置方式 結合下面源碼看 -->
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<package name="org.mybatis.builder"/>
</mappers>
複製代碼
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
// 注意在for循環裏每一個mapper都從新new一個XMLMapperBuilder,來解析
// 注意構建者裏面還傳入了 configuration
// 也就是說 mapper映射文件 對應的配置對象也須要封裝在 configuration中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 使用絕對url路徑
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
// 映射器比較複雜,調用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 使用java類名
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.");
}
}
}
}
}
複製代碼
咱們會對 mappers 標籤裏面的子標籤進行遍歷,對於除了 package 的三種資源標識(resource,url,class)來講,每一個 mapper 子標籤都會構建一個 XMLMapperBuilder 去構建解析對應的 mapper 映射配置文件。其實這些 資源標誌 就是讓程序去尋找到對應的 xxxMapper.xml 映射文件,而後一樣適用構建者模式去構建 xxxMapper.xml 對應的配置對象。
咱們來看一下 XmlMapperBuilder 構建者是如何構建相應的 「Mapper」 配置對象的。
public void parse() {
// 若是沒有加載過再加載,防止重複加載
if (!configuration.isResourceLoaded(resource)) {
//主線在這裏 配置 mapper
configurationElement(parser.evalNode("/mapper"));
// 標記一下,已經加載過了
configuration.addLoadedResource(resource);
// 綁定映射器到namespace
bindMapperForNamespace();
}
// 能夠忽略
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
//1.配置namespace
// 這步驟也是挺關鍵 先記住 namespace這個東西
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 緩存 能夠先無論
//2.配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已經廢棄,老式風格的參數映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// mybatis 很是很是重要的功能
//5.配置resultMap(高級功能)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定義可重用的 SQL 代碼段)
sqlElement(context.evalNodes("/mapper/sql"));
//7.配置select|insert|update|delete
// 這裏是真正的主線
// 這裏會根據前面的sql片斷建立在Mapper中真正的配置對象 MappedStatement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
// 傳入 select|insert|update|delete 標籤的 節點列表進行構建 Statement
private void buildStatementFromContext(List<XNode> list) {
// 判斷DatabaseId
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 都是調用這個方法
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 構建全部語句,一個mapper下能夠有不少select
// 這裏又使用了構造者模式
// 語句比較複雜,核心都在這裏面,因此調用XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 主線
// 核心XMLStatementBuilder.parseStatementNode
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 若是出現SQL語句不完整,把它記下來,塞到configuration去
configuration.addIncompleteStatement(statementParser);
}
}
}
複製代碼
上面那麼長的一大串代碼其實就是一個鏈式調用。咱們畫一下流程便於你理解。
接下來就到了 XMLStatementBuilder
這個類中去構建 MappedStatement
對象了。
// 緊接着上面的解析構建方法
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 若是databaseId不匹配,退出
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//暗示驅動程序每次批量返回的結果行數
Integer fetchSize = context.getIntAttribute("fetchSize");
//超時時間
Integer timeout = context.getIntAttribute("timeout");
//引用外部 parameterMap,已廢棄
String parameterMap = context.getStringAttribute("parameterMap");
// 前面三個不過重要
//參數類型 這個在參數映射的時候挺重要
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//引用外部的 resultMap(高級功能) 很是重要了
//算是 Mybatis 中核心功能了
String resultMap = context.getStringAttribute("resultMap");
//結果類型
String resultType = context.getStringAttribute("resultType");
//腳本語言,mybatis3.2的新功能 不重要
String lang = context.getStringAttribute("lang");
//獲得語言驅動 不重要
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
//結果集類型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一種
String resultSetType = context.getStringAttribute("resultSetType");
//語句類型, STATEMENT|PREPARED|CALLABLE 的一種
// 獲取 Statement類型 這個是須要和 JDBC作映射的
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//獲取命令類型(select|insert|update|delete)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否要緩存select結果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//僅針對嵌套結果 select 語句適用:若是爲 true,就是假設包含了嵌套結果集或是分組了,這樣的話當返回一個主結果行的時候,就不會發生有對前面結果集的引用的狀況。
//這就使得在獲取嵌套的結果集的時候不至於致使內存不夠用。默認值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
// 解析以前先解析<include>SQL片斷 這個在前面 XMLMapperBuilder
// 中已經構建完成,這裏須要調用並解析
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//解析以前先解析<selectKey> selectKey主要涉及須要某些特殊關係來設置主鍵的值
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 作了解
//解析成SqlSource,通常是DynamicSqlSource
// 其實無論 Dynamic 仍是 Raw 最終都會解析成 static
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
//(僅對 insert 有用) 標記一個屬性, MyBatis 會經過 getGeneratedKeys 或者經過 insert 語句的 selectKey 子元素設置它的值
String keyProperty = context.getStringAttribute("keyProperty");
//(僅對 insert 有用) 標記一個屬性, MyBatis 會經過 getGeneratedKeys 或者經過 insert 語句的 selectKey 子元素設置它的值
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
// 真正的主線在這裏
//調用助手類去真正建立MappedStatement而後加入配置Configuration中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
複製代碼
這個 parseStatementNode 方法比較長,但其實你能夠發現這裏無非就是對 每一個 CRUD(這裏指 select delete update insert標籤) 標籤作了具體的解析,其中比較重要的就幾種,好比 ParameterType,ResultMap,解析成SqlSource(Sql的封裝),sql片斷的解析。。。 其餘的其實都是支線了,你能夠自行去了解。
在作完屬性的一些解析後,XMLStatementBuilder
會將這些屬性再 委託 給助手對象 MapperBuilderAssistant
去進行構建 MappedStatement
。
// 好多參數。。
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
//爲id加上namespace前綴
// 這裏就有意思了
// 還記的上面說的 namespace 嗎?
// 這裏會使用 CRUD自己標籤的id 加上namespace構建獨一無二的id
// 主要是由於全部mapper文件中的 crud 標籤配置對象都是直接存儲
// 在 configuration 中的 ,爲了防止有些 標籤id會重複
id = applyCurrentNamespace(id, false);
// 是不是select語句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 很經典的構造者模式了,返回須要被構建的對象就能夠鏈式調用
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);
//1.參數映射 這裏很重要
// 由於 parameterMap 被棄用 因此這裏通常爲空
// 而真正傳入的其實應該是 parameterType 這個 Class
setStatementParameterMap(parameterMap, parameterType, statementBuilder);
//2.結果映射 也很重要
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
MappedStatement statement = statementBuilder.build();
//建造好調用configuration.addMappedStatement
// 加入configuration 這裏整個構建流程就算基本結束了。。
configuration.addMappedStatement(statement);
return statement;
}
複製代碼
咱們發現最終是經過 XMLMapperBuilder
的助手類去構建 MappedStatement
並傳入 Configuration
中的。咱們這時候能夠將上面一張流程圖更加細化一些。
咱們來看一下剛剛的還未分析完的參數映射代碼
private void setStatementParameterMap( String parameterMap, Class<?> parameterTypeClass, MappedStatement.Builder statementBuilder) {
// 給parameterMap加上namespace 可是由於parameterMap被棄用 因此通常返回null
parameterMap = applyCurrentNamespace(parameterMap, true);
if (parameterMap != null) {
try {
statementBuilder.parameterMap(configuration.getParameterMap(parameterMap));
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find parameter map " + parameterMap, e);
}
} else if (parameterTypeClass != null) {
// 解析 parameterType生成的類對象
List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
// 構造ParameterMap類內部的構建類
// 這裏主要是 parameterTypeClass 的賦值 而parameterMapping僅做爲一個空列表傳入
ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(
configuration,
statementBuilder.id() + "-Inline",
parameterTypeClass,
parameterMappings);
// 經過內部構建類構建ParameterMap並傳入配置對象中
statementBuilder.parameterMap(inlineParameterMapBuilder.build());
}
}
複製代碼
由於 parameterMap
棄用,因此設置參數大部分是圍繞着 parameterType
走的,總結來講就是經過 parameterType
去構建一個 ParameterMap
對象(這裏是使用的ParameterMap
中的內部構建者構建的)。而後將這個 ParameterMap
對象存儲在 MappedStatement
中。
其實這個 ParameterMap
對象也就三個字段,甚至咱們僅僅須要兩個。我這裏簡單寫一個 ParameterMap 類。
public class ParameterMap {
private String id;
private Class<?> type;
// 其實若是 parameterMapping 棄用了這個字段也沒什麼用了
// 估計後面會進行重構
private List<ParameterMapping> parameterMappings;
}
複製代碼
官方文檔已經要刪除這個元素了。
說完了參數映射,其實結果映射也大同小異。
private void setStatementResultMap( String resultMap, Class<?> resultType, ResultSetType resultSetType, MappedStatement.Builder statementBuilder) {
// 應用 namespace
resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
if (resultMap != null) {
// 進行ResultMap的解析
// 這裏經過,分割 你能夠寫成 xxxResultMap,xxxResultMap 但我還沒發現有人使用過
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
try {
// 這裏其實就是經過 resultMapName
// 去原來已經在 configuration解析完成的 <resultMap> 標籤
// 配置中獲取相應的 resultMap而後加入 resultMaps中
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find result map " + resultMapName, e);
}
}
} else if (resultType != null) {
// resultType解析
//<select id="selectUsers" resultType="User">
//這種狀況下,MyBatis 會在幕後自動建立一個 ResultMap,基於屬性名來映射列到 JavaBean 的屬性上。
//若是列名沒有精確匹配,你能夠在列名上使用 select 字句的別名來匹配標籤。
//建立一個inline result map, 把resultType設上就OK了,
//而後後面被DefaultResultSetHandler.createResultObject()使用
//DefaultResultSetHandler.getRowValue()使用
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
configuration,
statementBuilder.id() + "-Inline",
resultType,
new ArrayList<ResultMapping>(),
null);
// 最後仍是封裝成了 resultMap 集合
resultMaps.add(inlineResultMapBuilder.build());
}
// 將 resultMap 集合加入配置
statementBuilder.resultMaps(resultMaps);
// 這個直接加入配置
statementBuilder.resultSetType(resultSetType);
}
複製代碼
最新版本的結果映射寫在了構建流程中。
總的說來也就是 獲取 resultMap或者resultType中的值 而後經過這個值構建ResultMap傳入 MappedStatement配置中去。
這個時候咱們就能夠畫出大概的 MappedStatement
對象的構建流程圖了。
其實整個 Mybatis 初始化流程就是 對配置文件進行解析成配置對象裝入內存以便在後面執行的過程當中使用。而 mybatis 的配置文件會存儲到對應的 Configuration
對象中,而映射配置文件會專門解析 CRUD
標籤存入 MappedStatement
對象中,最終這個 MappedStatement
對象也會加入集合並存入到 Configuration
中去。這其中主要用到了 工廠模式和構建者模式。
Mybatis主要有兩個執行模塊,第一就是這篇文章咱們所講的 構建部分,還有一部分就是 Sql的執行部分,關於執行部分我會在下一篇文章中分享。