前言
好久不見(jiàn),從上一篇文章過(guò)后将饺,休整了兩個(gè)月,又逢疫情特殊時(shí)期痛黎,天天宅在家里挺尸予弧,真是見(jiàn)證了一個(gè)人可以懶惰到什么境界。好吧廢話不多說(shuō)了湖饱,今天會(huì)給大家分享我們常用的持久層框架——MyBatis的工作原理和源碼解析掖蛤。
說(shuō)實(shí)話MyBatis是我第一個(gè)接觸的持久層框架,在這之前我也沒(méi)有用過(guò)Hibernate井厌,從Java原生的Jdbc操作數(shù)據(jù)庫(kù)之后就直接過(guò)渡到了這個(gè)框架上蚓庭,當(dāng)時(shí)給我的第一感覺(jué)是,有一個(gè)框架太方便了仅仆,舉一個(gè)例子吧器赞,我們?cè)贘dbc操作的時(shí)候,對(duì)于對(duì)象的封裝墓拜,我們是需要通過(guò)ResultSet.getXXX(index)來(lái)獲取值港柜,然后在通過(guò)對(duì)象的setXXX()方法進(jìn)行手動(dòng)注入,這種重復(fù)且無(wú)任何技術(shù)含量的工作一直以來(lái)都是被我們程序猿所鄙視的一環(huán),而MyBatis就可以直接將我們的SQL查詢出來(lái)的數(shù)據(jù)與對(duì)象直接進(jìn)行映射然后直接返回一個(gè)封裝完成的對(duì)象夏醉,這節(jié)省了程序猿大部分的時(shí)間爽锥,當(dāng)然其實(shí)JdbcTemplate也可以做到,但是這里先不說(shuō)畔柔。MyBatis的優(yōu)點(diǎn)有非常多氯夷,當(dāng)然這也只有同時(shí)使用過(guò)Jdbc和MyBatis之后,產(chǎn)生對(duì)比靶擦,才會(huì)有這種巨大的落差感腮考,但這并不是今天要討論的重點(diǎn),今天的重心還是放在MyBatis是如何做到這些的奢啥。
對(duì)于MyBatis秸仙,給我個(gè)人的感受,其工作流程實(shí)際上分為兩部分:第一桩盲,構(gòu)建寂纪,也就是解析我們寫的xml配置,將其變成它所需要的對(duì)象赌结。第二捞蛋,就是執(zhí)行,在構(gòu)建完成的基礎(chǔ)上柬姚,去執(zhí)行我們的SQL拟杉,完成與Jdbc的交互。而這篇的重點(diǎn)會(huì)先放在構(gòu)建上量承。
Xml配置文件
玩過(guò)這個(gè)框架的同學(xué)都知道搬设,我們?cè)趩为?dú)使用它的時(shí)候,會(huì)需要兩個(gè)配置文件撕捍,分別是mybatis-config.xml和mapper.xml拿穴,在官網(wǎng)上可以直接看到,當(dāng)然這里為了方便忧风,我就直接將我的xml配置復(fù)制一份默色。
<!-- 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事務(wù)管理 -->
<transactionManager type="JDBC" />
<!-- 數(shù)據(jù)庫(kù)連接池 -->
<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這個(gè)文件主要是用于配置數(shù)據(jù)源狮腿、配置別名腿宰、加載mapper.xml,并且我們可以看到這個(gè)文件的<mappers>
節(jié)點(diǎn)中包含了一個(gè)<mapper>
缘厢,而這個(gè)mapper所指向的路徑就是另外一個(gè)xml文件:DemoMapper.xml吃度,而這個(gè)文件中寫了我們查詢數(shù)據(jù)庫(kù)所用的SQL。
而昧绣,MyBatis實(shí)際上就是將這兩個(gè)xml文件规肴,解析成配置對(duì)象,在執(zhí)行中去使用它。
解析
-
MyBatis需要什么配置對(duì)象拖刃?
雖然在這里我們并沒(méi)有進(jìn)行源碼的閱讀删壮,但是作為一個(gè)程序猿,我們可以憑借日常的開發(fā)經(jīng)驗(yàn)做出一個(gè)假設(shè)兑牡。假設(shè)來(lái)源于問(wèn)題其监,那么問(wèn)題就是:為什么要將配置和SQL語(yǔ)句分為兩個(gè)配置文件而不是直接寫在一起铅辞?
是不是就意味著,這兩個(gè)配置文件會(huì)被MyBatis分開解析成兩個(gè)不同的Java對(duì)象?
不妨先將問(wèn)題擱置阅酪,進(jìn)行源碼的閱讀晶通。
-
環(huán)境搭建
首先我們可以寫一個(gè)最基本的使用MyBatis的代碼级野,我這里已經(jīng)寫好了棒厘。
public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //創(chuàng)建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(); }
看源碼重要的一點(diǎn)就是要找到源碼的入口,而我們可以從這幾行程序出發(fā)如迟,來(lái)看看構(gòu)建究竟是在哪開始的收毫。
首先不難看出,這段程序顯示通過(guò)字節(jié)流讀取了mybatis-config.xml文件殷勘,然后通過(guò)SqlSessionFactoryBuilder.build()方法此再,創(chuàng)建了一個(gè)SqlSessionFactory(這里用到了工廠模式和構(gòu)建者模式),前面說(shuō)過(guò)玲销,MyBatis就是通過(guò)我們寫的xml配置文件输拇,來(lái)構(gòu)建配置對(duì)象的,那么配置文件所在的地方贤斜,就一定是構(gòu)建開始的地方策吠,也就是build方法。
-
構(gòu)建開始
進(jìn)入build方法瘩绒,我們可以看到這里的確有解析的意思,這個(gè)方法返回了一個(gè)SqlSessionFactory奴曙,而這個(gè)對(duì)象也是使用構(gòu)造者模式創(chuàng)建的,不妨繼續(xù)往下走草讶。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //解析mybatis-config.xml //XMLConfigBuilder 構(gòu)造者 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //parse(): 解析mybatis-config.xml里面的節(jié)點(diǎn) 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. } } }
進(jìn)入parse():
public Configuration parse() { //查看該文件是否已經(jīng)解析過(guò) if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } //如果沒(méi)有解析過(guò),則繼續(xù)往下解析炉菲,并且將標(biāo)識(shí)符置為true parsed = true; //解析<configuration>節(jié)點(diǎn) parseConfiguration(parser.evalNode("/configuration")); return configuration; }
注意parse的返回值堕战,Configuration,這個(gè)似曾相識(shí)的單詞好像在哪見(jiàn)過(guò)拍霜,是否與mybatis-config.xml中的
<configuration>
節(jié)點(diǎn)有所關(guān)聯(lián)呢嘱丢?答案是肯定的,我們可以接著往下看祠饺。
看到這里越驻,雖然代碼量還不是特別多,但是至少現(xiàn)在我們可以在大腦中得到一個(gè)大致的主線圖,也如下圖所示:
大致構(gòu)建圖沿著這條主線缀旁,我們進(jìn)入parseConfiguration(XNode)方法记劈,接著往下看。
private void parseConfiguration(XNode root) { try { //解析<Configuration>下的節(jié)點(diǎn) //issue #117 read properties first //<properties> propertiesElement(root.evalNode("properties")); //<settings> Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); //別名<typeAliases>解析 // 所謂別名 其實(shí)就是把你指定的別名對(duì)應(yīng)的class存儲(chǔ)在一個(gè)Map當(dāng)中 typeAliasesElement(root.evalNode("typeAliases")); //插件 <plugins> pluginElement(root.evalNode("plugins")); //自定義實(shí)例化對(duì)象的行為<objectFactory> objectFactoryElement(root.evalNode("objectFactory")); //MateObject 方便反射操作實(shí)體類的對(duì)象 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); } }
可以看到這個(gè)方法已經(jīng)在解析
<configuration>
下的節(jié)點(diǎn)了并巍,例如<settings>
,<typeAliases>
,<environments>
和<mappers>
目木。這里主要使用了分步構(gòu)建,每個(gè)解析不同標(biāo)簽的方法內(nèi)部都對(duì)Configuration對(duì)象進(jìn)行了set或者其它類似的操作懊渡,經(jīng)過(guò)這些操作之后刽射,一個(gè)Configuration對(duì)象就構(gòu)建完畢了,這里由于代碼量比較大剃执,而且大多數(shù)構(gòu)建都是些細(xì)節(jié)誓禁,大概知道怎么用就可以了,就不在文章中說(shuō)明了肾档,我會(huì)挑一個(gè)主要的說(shuō)摹恰,當(dāng)然有興趣的同學(xué)可以自己去pull MyBatis的源碼看看。
-
Mappers
上文中提到阁最,mybatis-config.xml文件中我們一定會(huì)寫一個(gè)叫做
<mappers>
的標(biāo)簽戒祠,這個(gè)標(biāo)簽中的<mapper>
節(jié)點(diǎn)存放了我們對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作的SQL語(yǔ)句,所以這個(gè)標(biāo)簽的構(gòu)建會(huì)作為今天分析的重點(diǎn)速种。首先在看源碼之前姜盈,我們先回憶一下我們?cè)趍apper標(biāo)簽內(nèi)通常會(huì)怎樣進(jìn)行配置,通常有如下幾種配置方式配阵。
<mappers> <!-- 通過(guò)配置文件路徑 --> <mapper resource="mapper/DemoMapper.xml" ></mapper> <!-- 通過(guò)Java全限定類名 --> <mapper class="com.mybatistest.TestMapper"/> <!-- 通過(guò)url 通常是mapper不在本地時(shí)用 --> <mapper url=""/> <!-- 通過(guò)包名 --> <package name="com.mybatistest"/> <!-- 注意 mapper節(jié)點(diǎn)中馏颂,可以使用resource/url/class三種方式獲取mapper--> </mappers>
這是
<mappers>
標(biāo)簽的幾種配置方式,通過(guò)這幾種配置方式棋傍,可以幫助我們更容易理解mappers的解析救拉。private void mapperElement(XNode parent) throws Exception { if (parent != null) { //遍歷解析mappers下的節(jié)點(diǎn) for (XNode child : parent.getChildren()) { //首先解析package節(jié)點(diǎn) if ("package".equals(child.getName())) { //獲取包名 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { //如果不存在package節(jié)點(diǎn),那么掃描mapper節(jié)點(diǎn) //resource/url/mapperClass三個(gè)值只能有一個(gè)值是有值的 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); //優(yōu)先級(jí) resource>url>mapperClass if (resource != null && url == null && mapperClass == null) { //如果mapper節(jié)點(diǎn)中的resource不為空 ErrorContext.instance().resource(resource); //那么直接加載resource指向的XXXMapper.xml文件為字節(jié)流 InputStream inputStream = Resources.getResourceAsStream(resource); //通過(guò)XMLMapperBuilder解析XXXMapper.xml瘫拣,可以看到這里構(gòu)建的XMLMapperBuilde還傳入了configuration,所以之后肯定是會(huì)將mapper封裝到configuration對(duì)象中去的亿絮。 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //解析 mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { //如果url!=null,那么通過(guò)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麸拄,那么通過(guò)加載類構(gòu)造Configuration Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { //如果都不滿足 則直接拋異常 如果配置了兩個(gè)或三個(gè) 直接拋異常 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
通過(guò)resource解析我們的配置文件中寫的是通過(guò)resource來(lái)加載mapper.xml的派昧,所以會(huì)通過(guò)XMLMapperBuilder來(lái)進(jìn)行解析,我們可以進(jìn)去他的parse方法中看一下:
public void parse() { //判斷文件是否之前解析過(guò) if (!configuration.isResourceLoaded(resource)) { //解析mapper文件節(jié)點(diǎn)(主要)(下面貼了代碼) configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //綁定Namespace里面的Class對(duì)象 bindMapperForNamespace(); } //重新解析之前解析不了的節(jié)點(diǎn)拢切,先不看蒂萎,最后填坑。 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } //解析mapper文件里面的節(jié)點(diǎn) // 拿到里面配置的配置項(xiàng) 最終封裝成一個(gè)MapperedStatemanet private void configurationElement(XNode context) { try { //獲取命名空間 namespace淮椰,這個(gè)很重要五慈,后期mybatis會(huì)通過(guò)這個(gè)動(dòng)態(tài)代理我們的Mapper接口 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { //如果namespace為空則拋一個(gè)異常 throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); //解析緩存節(jié)點(diǎn) cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); //解析parameterMap(過(guò)時(shí))和resultMap <resultMap></resultMap> parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); //解析<sql>節(jié)點(diǎn) //<sql id="staticSql">select * from test</sql> (可重用的代碼段) //<select> <include refid="staticSql"></select> sqlElement(context.evalNodes("/mapper/sql")); //解析增刪改查節(jié)點(diǎn)<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); } }
在這個(gè)parse()方法中纳寂,調(diào)用了一個(gè)configuationElement代碼,用于解析XXXMapper.xml文件中的各種節(jié)點(diǎn)泻拦,包括
<cache>
毙芜、<cache-ref>
、<paramaterMap>
(已過(guò)時(shí))聪轿、<resultMap>
爷肝、<sql>
、還有增刪改查節(jié)點(diǎn)陆错,和上面相同的是灯抛,我們也挑一個(gè)主要的來(lái)說(shuō),因?yàn)榻馕鲞^(guò)程都大同小異音瓷。毋庸置疑的是对嚼,我們?cè)赬XXMapper.xml中必不可少的就是編寫SQL,與數(shù)據(jù)庫(kù)交互主要靠的也就是這個(gè)绳慎,所以著重說(shuō)說(shuō)解析增刪改查節(jié)點(diǎn)的方法——buildStatementFromContext()纵竖。
在沒(méi)貼代碼之前,根據(jù)這個(gè)名字就可以略知一二了杏愤,這個(gè)方法會(huì)根據(jù)我們的增刪改查節(jié)點(diǎn)靡砌,來(lái)構(gòu)造一個(gè)Statement,而用過(guò)原生Jdbc的都知道珊楼,Statement就是我們操作數(shù)據(jù)庫(kù)的對(duì)象通殃。
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節(jié)點(diǎn) statementParser.parseStatementNode(); } catch (IncompleteElementException e) { //xml語(yǔ)句有問(wèn)題時(shí) 存儲(chǔ)到集合中 等解析完能解析的再重新解析 configuration.addIncompleteStatement(statementParser); } } } public void parseStatementNode() { //獲取<select id="xxx">中的id String id = context.getStringAttribute("id"); //獲取databaseId 用于多數(shù)據(jù)庫(kù),這里為null String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } //獲取節(jié)點(diǎn)名 select update delete insert String nodeName = context.getNode().getNodeName(); //根據(jù)節(jié)點(diǎn)名厕宗,得到SQL操作的類型 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); //判斷是否是查詢 boolean isSelect = sqlCommandType == SqlCommandType.SELECT; //是否刷新緩存 默認(rèn):增刪改刷新 查詢不刷新 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); //是否使用二級(jí)緩存 默認(rèn)值:查詢使用 增刪改不使用 boolean useCache = context.getBooleanAttribute("useCache", isSelect); //是否需要處理嵌套查詢結(jié)果 group by // 三組數(shù)據(jù) 分成一個(gè)嵌套的查詢結(jié)果 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); //替換Includes標(biāo)簽為對(duì)應(yīng)的sql標(biāo)簽里面的值 includeParser.applyIncludes(context.getNode()); //獲取parameterType名 String parameterType = context.getStringAttribute("parameterType"); //獲取parameterType的Class Class<?> parameterTypeClass = resolveClass(parameterType); //解析配置的自定義腳本語(yǔ)言驅(qū)動(dòng) 這里為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) //設(shè)置主鍵自增規(guī)則 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(重要) 根據(jù)sql文本來(lái)判斷是否需要?jiǎng)討B(tài)解析 如果沒(méi)有動(dòng)態(tài)sql語(yǔ)句且 只有#{}的時(shí)候 直接靜態(tài)解析使用?占位 當(dāng)有 ${} 不解析 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); //獲取StatementType画舌,可以理解為Statement和PreparedStatement StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); //沒(méi)用過(guò) Integer fetchSize = context.getIntAttribute("fetchSize"); //超時(shí)時(shí)間 Integer timeout = context.getIntAttribute("timeout"); //已過(guò)時(shí) String parameterMap = context.getStringAttribute("parameterMap"); //獲取返回值類型名 String resultType = context.getStringAttribute("resultType"); //獲取返回值烈性的Class Class<?> resultTypeClass = resolveClass(resultType); //獲取resultMap的id String resultMap = context.getStringAttribute("resultMap"); //獲取結(jié)果集類型 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對(duì)象(代碼貼在下面) builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } //將剛才獲取到的屬性已慢,封裝成MappedStatement對(duì)象 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; //通過(guò)構(gòu)造者模式+鏈?zhǔn)阶兂汕簦瑯?gòu)造一個(gè)MappedStatement的構(gòu)造者 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); } //通過(guò)構(gòu)造者構(gòu)造MappedStatement MappedStatement statement = statementBuilder.build(); //將MappedStatement對(duì)象封裝到Configuration對(duì)象中 configuration.addMappedStatement(statement); return statement; }
這個(gè)代碼段雖然很長(zhǎng),但是一句話形容它就是繁瑣但不復(fù)雜佑惠,里面主要也就是對(duì)xml的節(jié)點(diǎn)進(jìn)行解析朋腋。舉個(gè)比上面簡(jiǎn)單的例子吧,假設(shè)我們有這樣一段配置:
<select id="selectDemo" parameterType="java.lang.Integer" resultType='Map'> SELECT * FROM test </select>
MyBatis需要做的就是膜楷,先判斷這個(gè)節(jié)點(diǎn)是用來(lái)干什么的乍丈,然后再獲取這個(gè)節(jié)點(diǎn)的id、parameterType把将、resultType等屬性,封裝成一個(gè)MappedStatement對(duì)象忆矛,由于這個(gè)對(duì)象很復(fù)雜察蹲,所以MyBatis使用了構(gòu)造者模式來(lái)構(gòu)造這個(gè)對(duì)象请垛,最后當(dāng)MappedStatement對(duì)象構(gòu)造完成后,將其封裝到Configuration對(duì)象中洽议。
代碼執(zhí)行至此宗收,基本就結(jié)束了對(duì)Configuration對(duì)象的構(gòu)建,MyBatis的第一階段:構(gòu)造亚兄,也就到這里結(jié)束了混稽,現(xiàn)在再來(lái)回答我們?cè)谖恼麻_頭提出的那兩個(gè)問(wèn)題:MyBatis需要構(gòu)造什么對(duì)象?以及是否兩個(gè)配置文件對(duì)應(yīng)著兩個(gè)對(duì)象审胚?匈勋,似乎就已經(jīng)有了答案,這里做一個(gè)總結(jié):
MyBatis需要對(duì)配置文件進(jìn)行解析膳叨,最終會(huì)解析成一個(gè)Configuration對(duì)象洽洁,但是要說(shuō)兩個(gè)配置文件對(duì)應(yīng)了兩個(gè)對(duì)象實(shí)際上也沒(méi)有錯(cuò):
- Configuration對(duì)象,保存了mybatis-config.xml的配置信息菲嘴。
- MappedStatement饿自,保存了XXXMapper.xml的配置信息。
但是最終MappedStatement對(duì)象會(huì)封裝到Configuration對(duì)象中龄坪,合二為一昭雌,成為一個(gè)單獨(dú)的對(duì)象,也就是Configuration健田。
最后給大家畫一個(gè)構(gòu)建過(guò)程的流程圖:
填坑
-
SQL語(yǔ)句在哪解析烛卧?
細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了,上文中只說(shuō)了去節(jié)點(diǎn)中獲取一些屬性從而構(gòu)建配置對(duì)象抄课,但是最重要的SQL語(yǔ)句并沒(méi)有提到唱星,這是因?yàn)檫@部分我想要和屬性區(qū)分開單獨(dú)說(shuō),由于MyBatis支持動(dòng)態(tài)SQL和${}跟磨、#{}的多樣的SQL间聊,所以這里單獨(dú)提出來(lái)說(shuō)會(huì)比較合適。
首先可以確認(rèn)的是抵拘,剛才我們走完的那一整個(gè)流程中哎榴,包含了SQL語(yǔ)句的生成,下面貼代碼(這一段代碼相當(dāng)繞僵蛛,不好讀)尚蝌。
//解析Sql(重要) 根據(jù)sql文本來(lái)判斷是否需要?jiǎng)討B(tài)解析 如果沒(méi)有動(dòng)態(tài)sql語(yǔ)句且 只有#{}的時(shí)候 直接靜態(tài)解析使用?占位 當(dāng)有 ${} 不解析 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
這里就是生成Sql的入口,以單步調(diào)試的角度接著往下看充尉。
/*進(jìn)入createSqlSource方法*/ @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { //進(jìn)入這個(gè)構(gòu)造 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); //進(jìn)入parseScriptNode return builder.parseScriptNode(); } /** 進(jìn)入這個(gè)方法 */ public SqlSource parseScriptNode() { //# //會(huì)先解析一遍 MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { //如果是${}會(huì)直接不解析飘言,等待執(zhí)行的時(shí)候直接賦值 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { //用占位符方式來(lái)解析 #{} --> ? sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<>(); //獲取select標(biāo)簽下的子標(biāo)簽 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語(yǔ)句 這里是 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 { //如果不是,則直接生成靜態(tài)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); }
/*從上面的代碼段到這一段中間需要經(jīng)過(guò)很多代碼驼侠,就不一段一段貼了*/ public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); //這里會(huì)生成一個(gè)GenericTokenParser姿鸿,傳入#{}作為開始和結(jié)束谆吴,然后調(diào)用其parse方法,即可將#{}換為 ? GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); //這里可以解析#{} 將其替換為? String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } //經(jīng)過(guò)一段復(fù)雜的解析過(guò)程 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()方法苛预,這個(gè)方法是核心 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 //當(dāng)掃描到${}的時(shí)候調(diào)用此方法 其實(shí)就是不解析 在運(yùn)行時(shí)候在替換成具體的值 @Override public String handleToken(String content) { this.isDynamic = true; return null; } //ParameterMappingTokenHandler的handleToken //全局掃描#{id} 字符串之后 會(huì)把里面所有 #{} 調(diào)用handleToken 替換為? @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; }
這段代碼相當(dāng)繞句狼,我們應(yīng)該站在一個(gè)宏觀的角度去看待它。所以我直接在這里概括一下:
首先這里會(huì)通過(guò)
<select>
節(jié)點(diǎn)獲取到我們的SQL語(yǔ)句热某,假設(shè)SQL語(yǔ)句中只有${}腻菇,那么直接就什么都不做,在運(yùn)行的時(shí)候直接進(jìn)行賦值昔馋。而如果掃描到了#{}字符串之后筹吐,會(huì)進(jìn)行替換,將#{}替換為 ?绒极。
那么他是怎么進(jìn)行判斷的呢骏令?
這里會(huì)生成一個(gè)GenericTokenParser,這個(gè)對(duì)象可以傳入一個(gè)openToken和closeToken垄提,如果是#{}榔袋,那么openToken就是#{,closeToken就是 }铡俐,然后通過(guò)parse方法中的handler.handleToken()方法進(jìn)行替換凰兑。
在這之前由于已經(jīng)進(jìn)行過(guò)SQL是否含有#{}的判斷了,所以在這里如果是只有${}审丘,那么handler就是BindingTokenParser的實(shí)例化對(duì)象吏够,如果存在#{},那么handler就是ParameterMappingTokenHandler的實(shí)例化對(duì)象滩报。
分別進(jìn)行處理锅知。
-
上文中提到的解析不了的節(jié)點(diǎn)是什么意思?
根據(jù)上文的代碼我們可知脓钾,解析Mapper.xml文件中的每個(gè)節(jié)點(diǎn)是有順序的售睹。
那么假設(shè)我寫了這么幾個(gè)節(jié)點(diǎn):
<select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'> </select> <resultMap id="demoResultMap" type="demo"> <id column property> <result coulmn property> </resultMap>
select節(jié)點(diǎn)是需要獲取resultMap的,但是此時(shí)resultMap并沒(méi)有被解析到可训,所以解析到
<select>
這個(gè)節(jié)點(diǎn)的時(shí)候是無(wú)法獲取到resultMap的信息的昌妹。我們來(lái)看看MyBatis是怎么做的:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //解析xml節(jié)點(diǎn) statementParser.parseStatementNode(); } catch (IncompleteElementException e) { //xml語(yǔ)句有問(wèn)題時(shí) 存儲(chǔ)到集合中 等解析完能解析的再重新解析 configuration.addIncompleteStatement(statementParser); } } }
當(dāng)解析到某個(gè)節(jié)點(diǎn)出現(xiàn)問(wèn)題的時(shí)候,會(huì)拋一個(gè)異常握截,然后會(huì)調(diào)用configuration的addIncompleteStatement方法飞崖,將這個(gè)解析對(duì)象先暫存到這個(gè)集合中,等到所有的節(jié)點(diǎn)都解析完畢之后谨胞,在對(duì)這個(gè)集合內(nèi)的解析對(duì)象繼續(xù)解析:
public void parse() { //判斷文件是否之前解析過(guò) if (!configuration.isResourceLoaded(resource)) { //解析mapper文件 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //綁定Namespace里面的Class對(duì)象 bindMapperForNamespace(); } //重新解析之前解析不了的節(jié)點(diǎn) 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); }
結(jié)語(yǔ)
至此整個(gè)MyBatis的查詢前構(gòu)建的過(guò)程就基本說(shuō)完了固歪,簡(jiǎn)單地總結(jié)就是,MyBatis會(huì)在執(zhí)行查詢之前胯努,對(duì)配置文件進(jìn)行解析成配置對(duì)象:Configuration昼牛,以便在后面執(zhí)行的時(shí)候去使用术瓮,而存放SQL的xml又會(huì)解析成MappedStatement對(duì)象,但是最終這個(gè)對(duì)象也會(huì)加入Configuration中贰健,至于Configuration是如何被使用的,以及SQL的執(zhí)行部分恬汁,我會(huì)在下一篇說(shuō)SQL執(zhí)行的時(shí)候分享伶椿。
歡迎大家訪問(wèn)我的個(gè)人博客:Object's Blog