前言
好久不見,從上一篇文章過後,休整了兩個月,又逢疫情特殊時期,天天宅在家裏挺屍,真是見證了一個人可以懶惰到什麼境界。好吧廢話不多說了,今天會給大家分享我們常用的持久層框架——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執行的時候分享。
歡迎大家訪問我的個人博客:Object’s Blog