前言
今天會給大家分享我們常用的持久層框架——MyBatis的工作原理和源碼解析,后續會圍繞Mybatis框架做一些比較深入的講解,之后這部分內容會歸置到公眾號菜單欄:連載中…-框架分析中,歡迎探討!
說實話MyBatis是我第一個接觸的持久層框架,在這之前我也沒有用過Hibernate,從Java原生的Jdbc操作數據庫之后就直接過渡到了這個框架上,當時給我的第一感覺是,有一個框架太方便了。
舉一個例子吧,我們在Jdbc操作的時候,對於對象的封裝,我們是需要通過ResultSet.getXXX(index)
來獲取值,然后在通過對象的setXXX()
方法進行手動注入,這種重復且無任何技術含量的工作一直以來都是被我們程序猿所鄙視的一環,而MyBatis就可以直接將我們的SQL查詢出來的數據與對象直接進行映射然后直接返回一個封裝完成的對象,這節省了程序猿大部分的時間,當然其實JdbcTemplate也可以做到,但是這里先不說。
MyBatis的優點有非常多,當然這也只有同時使用過Jdbc和MyBatis之后,產生對比,才會有這種巨大的落差感,但這並不是今天要討論的重點,今天的重心還是放在MyBatis是如何做到這些的。
對於MyBatis,給我個人的感受,其工作流程實際上分為兩部分:第一,構建,也就是解析我們寫的xml配置,將其變成它所需要的對象。第二,就是執行,在構建完成的基礎上,去執行我們的SQL,完成與Jdbc的交互。而這篇的重點會先放在構建上。
Xml配置文件
玩過這個框架的同學都知道,我們在單獨使用它的時候,會需要兩個配置文件,分別是mybatis-config.xml和mapper.xml,在官網上可以直接看到,當然這里為了方便,我就直接將我的xml配置復制一份。
<!-- mybatis-config.xml -->
<?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>
<!-- 和spring整合后 environments配置將廢除 -->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理 -->
<transactionManager type="JDBC" />
<!-- 數據庫連接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://xxxxxxx:3306/test?characterEncoding=utf8"/>
<property name="username" value="username" />
<property name="password" value="password" />
</dataSource>
</environment>
</environments>
<!-- 加載mapper.xml -->
<mappers>
<!-- <package name=""> -->
<mapper resource="mapper/DemoMapper.xml" ></mapper>
</mappers>
</configuration>
<!-- DemoMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.DemoMapper">
<select id="queryTest" parameterType="Map" resultType="Map">
select * from test WHERE id =#{id}
</select>
</mapper>
我們不難看出,在mybatis-config.xml這個文件主要是用於配置數據源、配置別名、加載mapper.xml,並且我們可以看到這個文件的<mappers>
節點中包含了一個<mapper>
,而這個mapper所指向的路徑就是另外一個xml文件:DemoMapper.xml,而這個文件中寫了我們查詢數據庫所用的SQL。
而,MyBatis實際上就是將這兩個xml文件,解析成配置對象,在執行中去使用它。
解析
MyBatis需要什么配置對象?
雖然在這里我們並沒有進行源碼的閱讀,但是作為一個程序猿,我們可以憑借日常的開發經驗做出一個假設。假設來源於問題,那么問題就是:為什么要將配置和SQL語句分為兩個配置文件而不是直接寫在一起?
是不是就意味着,這兩個配置文件會被MyBatis分開解析成兩個不同的Java對象?
不妨先將問題擱置,進行源碼的閱讀。
環境搭建
首先我們可以寫一個最基本的使用MyBatis的代碼,我這里已經寫好了。
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//創建SqlSessionFacory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
/******************************分割線******************************/
SqlSession sqlSession = sqlSessionFactory.openSession();
//獲取Mapper
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","123");
System.out.println(mapper.selectAll(map));
sqlSession.close();
sqlSession.commit();
}
看源碼重要的一點就是要找到源碼的入口,而我們可以從這幾行程序出發,來看看構建究竟是在哪開始的。
首先不難看出,這段程序顯示通過字節流讀取了mybatis-config.xml文件,然后通過SqlSessionFactoryBuilder.build()
方法,創建了一個SqlSessionFactory(這里用到了工廠模式和構建者模式),前面說過,MyBatis就是通過我們寫的xml配置文件,來構建配置對象的,那么配置文件所在的地方,就一定是構建開始的地方,也就是build方法。
構建開始
進入build方法,我們可以看到這里的確有解析的意思,這個方法返回了一個SqlSessionFactory,而這個對象也是使用構造者模式創建的,不妨繼續往下走。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//解析mybatis-config.xml
//XMLConfigBuilder 構造者
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//parse(): 解析mybatis-config.xml里面的節點
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.
}
}
}
進入parse():
public Configuration parse() {
//查看該文件是否已經解析過
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
//如果沒有解析過,則繼續往下解析,並且將標識符置為true
parsed = true;
//解析<configuration>節點
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
注意parse的返回值,Configuration,這個似曾相識的單詞好像在哪見過,是否與mybatis-config.xml中的<configuration>
節點有所關聯呢?
答案是肯定的,我們可以接着往下看。
看到這里,雖然代碼量還不是特別多,但是至少現在我們可以在大腦中得到一個大致的主線圖,也如下圖所示:
沿着這條主線,我們進入parseConfiguration(XNode)方法,接着往下看。
private void parseConfiguration(XNode root) {
try {
//解析<Configuration>下的節點
//issue #117 read properties first
//<properties>
propertiesElement(root.evalNode("properties"));
//<settings>
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//別名<typeAliases>解析
// 所謂別名 其實就是把你指定的別名對應的class存儲在一個Map當中
typeAliasesElement(root.evalNode("typeAliases"));
//插件 <plugins>
pluginElement(root.evalNode("plugins"));
//自定義實例化對象的行為<objectFactory>
objectFactoryElement(root.evalNode("objectFactory"));
//MateObject 方便反射操作實體類的對象
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//<environments>
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// typeHandlers
typeHandlerElement(root.evalNode("typeHandlers"));
//主要 <mappers> 指向我們存放SQL的xxxxMapper.xml文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
可以看到這個方法已經在解析<configuration>
下的節點了,例如<settings>
,<typeAliases>
,<environments>
和<mappers>
。
這里主要使用了分步構建,每個解析不同標簽的方法內部都對Configuration對象進行了set或者其它類似的操作,經過這些操作之后,一個Configuration對象就構建完畢了,這里由於代碼量比較大,而且大多數構建都是些細節,大概知道怎么用就可以了,就不在文章中說明了,我會挑一個主要的說,當然有興趣的同學可以自己去pull MyBatis的源碼看看。
Mappers
上文中提到,mybatis-config.xml文件中我們一定會寫一個叫做<mappers>的標簽,這個標簽中的<mapper>
節點存放了我們對數據庫進行操作的SQL語句,所以這個標簽的構建會作為今天分析的重點。
首先在看源碼之前,我們先回憶一下我們在mapper標簽內通常會怎樣進行配置,通常有如下幾種配置方式。
<mappers>
<!-- 通過配置文件路徑 -->
<mapper resource="mapper/DemoMapper.xml" ></mapper>
<!-- 通過Java全限定類名 -->
<mapper class="com.mybatistest.TestMapper"/>
<!-- 通過url 通常是mapper不在本地時用 -->
<mapper url=""/>
<!-- 通過包名 -->
<package name="com.mybatistest"/>
<!-- 注意 mapper節點中,可以使用resource/url/class三種方式獲取mapper-->
</mappers>
這是<mappers>
標簽的幾種配置方式,通過這幾種配置方式,可以幫助我們更容易理解mappers的解析。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//遍歷解析mappers下的節點
for (XNode child : parent.getChildren()) {
//首先解析package節點
if ("package".equals(child.getName())) {
//獲取包名
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//如果不存在package節點,那么掃描mapper節點
//resource/url/mapperClass三個值只能有一個值是有值的
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//優先級 resource>url>mapperClass
if (resource != null && url == null && mapperClass == null) {
//如果mapper節點中的resource不為空
ErrorContext.instance().resource(resource);
//那么直接加載resource指向的XXXMapper.xml文件為字節流
InputStream inputStream = Resources.getResourceAsStream(resource);
//通過XMLMapperBuilder解析XXXMapper.xml,可以看到這里構建的XMLMapperBuilde還傳入了configuration,所以之后肯定是會將mapper封裝到configuration對象中去的。
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//如果url!=null,那么通過url解析
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) {
//如果mapperClass!=null,那么通過加載類構造Configuration
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.");
}
}
}
}
}
我們的配置文件中寫的是通過resource來加載mapper.xml的,所以會通過XMLMapperBuilder來進行解析,我們可以進去他的parse方法中看一下:
public void parse() {
//判斷文件是否之前解析過
if (!configuration.isResourceLoaded(resource)) {
//解析mapper文件節點(主要)(下面貼了代碼)
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//綁定Namespace里面的Class對象
bindMapperForNamespace();
}
//重新解析之前解析不了的節點,先不看,最后填坑。
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
//解析mapper文件里面的節點
// 拿到里面配置的配置項 最終封裝成一個MapperedStatemanet
private void configurationElement(XNode context) {
try {
//獲取命名空間 namespace,這個很重要,后期mybatis會通過這個動態代理我們的Mapper接口
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
//如果namespace為空則拋一個異常
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//解析緩存節點
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//解析parameterMap(過時)和resultMap <resultMap></resultMap>
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析<sql>節點
//<sql id="staticSql">select * from test</sql> (可重用的代碼段)
//<select> <include refid="staticSql"></select>
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);
}
}
在這個parse()方法中,調用了一個configuationElement代碼,用於解析XXXMapper.xml文件中的各種節點,包括<cache>
、<cache-ref>
、<paramaterMap>
(已過時)、<resultMap>
、<sql>
、還有增刪改查節點,和上面相同的是,我們也挑一個主要的來說,因為解析過程都大同小異。
毋庸置疑的是,我們在XXXMapper.xml中必不可少的就是編寫SQL,與數據庫交互主要靠的也就是這個,所以着重說說解析增刪改查節點的方法——buildStatementFromContext()。
在沒貼代碼之前,根據這個名字就可以略知一二了,這個方法會根據我們的增刪改查節點,來構造一個Statement,而用過原生Jdbc的都知道,Statement就是我們操作數據庫的對象。
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
//解析xml
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析xml節點
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//xml語句有問題時 存儲到集合中 等解析完能解析的再重新解析
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
//獲取<select id="xxx">中的id
String id = context.getStringAttribute("id");
//獲取databaseId 用於多數據庫,這里為null
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//獲取節點名 select update delete insert
String nodeName = context.getNode().getNodeName();
//根據節點名,得到SQL操作的類型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//判斷是否是查詢
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//是否刷新緩存 默認:增刪改刷新 查詢不刷新
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否使用二級緩存 默認值:查詢使用 增刪改不使用
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//是否需要處理嵌套查詢結果 group by
// 三組數據 分成一個嵌套的查詢結果
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//替換Includes標簽為對應的sql標簽里面的值
includeParser.applyIncludes(context.getNode());
//獲取parameterType名
String parameterType = context.getStringAttribute("parameterType");
//獲取parameterType的Class
Class<?> parameterTypeClass = resolveClass(parameterType);
//解析配置的自定義腳本語言驅動 這里為null
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
//解析selectKey
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//設置主鍵自增規則
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))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
/************************************************************************************/
//解析Sql(重要) 根據sql文本來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?占位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//獲取StatementType,可以理解為Statement和PreparedStatement
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//沒用過
Integer fetchSize = context.getIntAttribute("fetchSize");
//超時時間
Integer timeout = context.getIntAttribute("timeout");
//已過時
String parameterMap = context.getStringAttribute("parameterMap");
//獲取返回值類型名
String resultType = context.getStringAttribute("resultType");
//獲取返回值烈性的Class
Class<?> resultTypeClass = resolveClass(resultType);
//獲取resultMap的id
String resultMap = context.getStringAttribute("resultMap");
//獲取結果集類型
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//將剛才獲取到的屬性,封裝成MappedStatement對象(代碼貼在下面)
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
//將剛才獲取到的屬性,封裝成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
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//通過構造者模式+鏈式變成,構造一個MappedStatement的構造者
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
//通過構造者構造MappedStatement
MappedStatement statement = statementBuilder.build();
//將MappedStatement對象封裝到Configuration對象中
configuration.addMappedStatement(statement);
return statement;
}
這個代碼段雖然很長,但是一句話形容它就是繁瑣但不復雜,里面主要也就是對xml的節點進行解析。舉個比上面簡單的例子吧,假設我們有這樣一段配置:
<select id="selectDemo" parameterType="java.lang.Integer" resultType='Map'>
SELECT * FROM test
</select>
MyBatis需要做的就是,先判斷這個節點是用來干什么的,然后再獲取這個節點的id、parameterType、resultType等屬性,封裝成一個MappedStatement對象,由於這個對象很復雜,所以MyBatis使用了構造者模式來構造這個對象,最后當MappedStatement對象構造完成后,將其封裝到Configuration對象中。
代碼執行至此,基本就結束了對Configuration對象的構建,MyBatis的第一階段:構造,也就到這里結束了,現在再來回答我們在文章開頭提出的那兩個問題:MyBatis需要構造什么對象?以及是否兩個配置文件對應着兩個對象?,似乎就已經有了答案,這里做一個總結:
MyBatis需要對配置文件進行解析,最終會解析成一個Configuration對象,但是要說兩個配置文件對應了兩個對象實際上也沒有錯:
-
Configuration對象,保存了mybatis-config.xml的配置信息。
-
MappedStatement,保存了XXXMapper.xml的配置信息。
但是最終MappedStatement對象會封裝到Configuration對象中,合二為一,成為一個單獨的對象,也就是Configuration。
最后給大家畫一個構建過程的流程圖:
填坑
SQL語句在哪解析?
細心的同學可能已經發現了,上文中只說了去節點中獲取一些屬性從而構建配置對象,但是最重要的SQL語句並沒有提到,這是因為這部分我想要和屬性區分開單獨說,由於MyBatis支持動態SQL和${}
、#{}
的多樣的SQL,所以這里單獨提出來說會比較合適。
首先可以確認的是,剛才我們走完的那一整個流程中,包含了SQL語句的生成,下面貼代碼(這一段代碼相當繞,不好讀)。
//解析Sql(重要) 根據sql文本來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?占位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
這里就是生成Sql的入口,以單步調試的角度接着往下看。
/*進入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//進入這個構造
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//進入parseScriptNode
return builder.parseScriptNode();
}
/**
進入這個方法
*/
public SqlSource parseScriptNode() {
//#
//會先解析一遍
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//如果是${}會直接不解析,等待執行的時候直接賦值
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//用占位符方式來解析 #{} --> ?
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//獲取select標簽下的子標簽
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
//如果是查詢
//獲取原生SQL語句 這里是 select * from test where id = #{id}
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//檢查sql是否是${}
if (textSqlNode.isDynamic()) {
//如果是${}那么直接不解析
contents.add(textSqlNode);
isDynamic = true;
} else {
//如果不是,則直接生成靜態SQL
//#{} -> ?
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//如果是增刪改
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
/*從上面的代碼段到這一段中間需要經過很多代碼,就不一段一段貼了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//這里會生成一個GenericTokenParser,傳入#{}作為開始和結束,然后調用其parse方法,即可將#{}換為 ?
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//這里可以解析#{} 將其替換為?
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
//經過一段復雜的解析過程
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
//遍歷里面所有的#{} select ? ,#{id1} ${}
while (start > -1) {
if (start > 0 && src[start - 1] == '\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//使用占位符 ?
//注意handler.handleToken()方法,這個方法是核心
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
//BindingTokenParser 的handleToken
//當掃描到${}的時候調用此方法 其實就是不解析 在運行時候在替換成具體的值
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
//ParameterMappingTokenHandler的handleToken
//全局掃描#{id} 字符串之后 會把里面所有 #{} 調用handleToken 替換為?
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
這段代碼相當繞,我們應該站在一個宏觀的角度去看待它。所以我直接在這里概括一下:
首先這里會通過<select>
節點獲取到我們的SQL語句,假設SQL語句中只有${}
,那么直接就什么都不做,在運行的時候直接進行賦值。
而如果掃描到了#{}
字符串之后,會進行替換,將#{}
替換為 ?
。
那么他是怎么進行判斷的呢?
這里會生成一個GenericTokenParser,這個對象可以傳入一個openToken和closeToken,如果是#{}
,那么openToken就是#{
,closeToken就是 }
,然后通過parse方法中的handler.handleToken()
方法進行替換。
在這之前由於已經進行過SQL是否含有#{}
的判斷了,所以在這里如果是只有${}
,那么handler就是BindingTokenParser的實例化對象,如果存在#{}
,那么handler就是ParameterMappingTokenHandler的實例化對象。
分別進行處理。
上文中提到的解析不了的節點是什么意思?
根據上文的代碼我們可知,解析Mapper.xml文件中的每個節點是有順序的。
那么假設我寫了這么一個幾個節點:
<select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'>
</select>
<resultMap id="demoResultMap" type="demo">
<id column property>
<result coulmn property>
</resultMap>
select節點是需要獲取resultMap的,但是此時resultMap並沒有被解析到,所以解析到<select>
這個節點的時候是無法獲取到resultMap的信息的。
我們來看看MyBatis是怎么做的:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//解析xml節點
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//xml語句有問題時 存儲到集合中 等解析完能解析的再重新解析
configuration.addIncompleteStatement(statementParser);
}
}
}
當解析到某個節點出現問題的時候,會拋一個異常,然后會調用configuration的addIncompleteStatement方法,將這個解析對象先暫存到這個集合中,等到所有的節點都解析完畢之后,在對這個集合內的解析對象繼續解析:
public void parse() {
//判斷文件是否之前解析過
if (!configuration.isResourceLoaded(resource)) {
//解析mapper文件
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//綁定Namespace里面的Class對象
bindMapperForNamespace();
}
//重新解析之前解析不了的節點
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void parsePendingResultMaps() {
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {
Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
while (iter.hasNext()) {
try {
//添加resultMap
iter.next().resolve();
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
}
}
}
}
public ResultMap resolve() {
//添加resultMap
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}
結語
至此整個MyBatis的查詢前構建的過程就基本說完了,簡單地總結就是,MyBatis會在執行查詢之前,對配置文件進行解析成配置對象:Configuration,以便在后面執行的時候去使用,而存放SQL的xml又會解析成MappedStatement對象,但是最終這個對象也會加入Configuration中。
至於Configuration是如何被使用的,以及SQL的執行部分,我會在下一篇說SQL執行的時候分享。