版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
MyBatis架構(gòu)與原理深入解析1引言本文主要講解JDBC怎么演變到Mybatis的漸變過程,重點(diǎn)講解了為什么要將JDBC封裝成Mybaits這樣一個(gè)持久層框架。再而論述Mybatis作為一個(gè)數(shù)據(jù)持久層框架本身有待改進(jìn)之處。2JDBC實(shí)現(xiàn)查詢分析我們先看看我們最熟悉也是最基礎(chǔ)的通過JDBC查詢數(shù)據(jù)庫(kù)數(shù)據(jù),一般需要以下七個(gè)步驟:加載JDBC驅(qū)動(dòng);建立并獲取數(shù)據(jù)庫(kù)連接;創(chuàng)建JDBCStatements對(duì)象;設(shè)置SQL語(yǔ)句的傳入?yún)?shù);執(zhí)行SQL語(yǔ)句并獲得查詢結(jié)果;對(duì)查詢結(jié)果進(jìn)行轉(zhuǎn)換處理并將處理結(jié)果返回;釋放相關(guān)資源(關(guān)閉Connection,關(guān)閉Statement,關(guān)閉ResultSet);以下是具體的實(shí)現(xiàn)代碼:publicstaticList<Map<String,Object>>queryForList(){Connectionconnection=null;ResultSetrs=null;PreparedStatementstmt=null;List<Map<String,Object>>resultList=newArrayList<Map<String,Object>>();try{//加載JDBC驅(qū)動(dòng)Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();Stringurl="jdbc:oracle:thin:@localhost:1521:ORACLEDB";Stringuser="trainer";Stringpassword="trainer";//獲取數(shù)據(jù)庫(kù)連接connection=DriverManager.getConnection(url,user,password);Stringsql="select*fromuserinfowhereuser_id=?";//創(chuàng)建Statement對(duì)象(每一個(gè)Statement為一次數(shù)據(jù)庫(kù)執(zhí)行請(qǐng)求)stmt=connection.prepareStatement(sql);//設(shè)置傳入?yún)?shù)stmt.setString(1,"zhangsan");//執(zhí)行SQL語(yǔ)句rs=stmt.executeQuery();//處理查詢結(jié)果(將查詢結(jié)果轉(zhuǎn)換成List格式)ResultSetMetaDatarsmd=rs.getMetaData();intnum=rsmd.getColumnCount();while(rs.next()){Mapmap=newHashMap();for(inti=0;i<num;i++){StringcolumnName=rsmd.getColumnName(i+1);map.put(columnName,rs.getString(columnName));}resultList.add(map);}}catch(Exceptione){e.printStackTrace();}finally{try{//關(guān)閉結(jié)果集if(rs!=null){rs.close();rs=null;}//關(guān)閉執(zhí)行if(stmt!=null){stmt.close();stmt=null;}if(connection!=null){connection.close();connection=null;}}catch(SQLExceptione){e.printStackTrace();}}returnresultList;}3JDBC演變到Mybatis過程上面我們看到了實(shí)現(xiàn)JDBC有七個(gè)步驟,哪些步驟是可以進(jìn)一步封裝的,減少我們開發(fā)的代碼量。3.1第一步優(yōu)化:連接獲取和釋放問題描述:數(shù)據(jù)庫(kù)連接頻繁的開啟和關(guān)閉本身就造成了資源的浪費(fèi),影響系統(tǒng)的性能。解決問題:數(shù)據(jù)庫(kù)連接的獲取和關(guān)閉我們可以使用數(shù)據(jù)庫(kù)連接池來(lái)解決資源浪費(fèi)的問題。通過連接池就可以反復(fù)利用已經(jīng)建立的連接去訪問數(shù)據(jù)庫(kù)了。減少連接的開啟和關(guān)閉的時(shí)間。問題描述:但是現(xiàn)在連接池多種多樣,可能存在變化,有可能采用DBCP的連接池,也有可能采用容器本身的JNDI數(shù)據(jù)庫(kù)連接池。解決問題:我們可以通過DataSource進(jìn)行隔離解耦,我們統(tǒng)一從DataSource里面獲取數(shù)據(jù)庫(kù)連接,DataSource具體由DBCP實(shí)現(xiàn)還是由容器的JNDI實(shí)現(xiàn)都可以,所以我們將DataSource的具體實(shí)現(xiàn)通過讓用戶配置來(lái)應(yīng)對(duì)變化。3.2第二步優(yōu)化:SQL統(tǒng)一存取問題描述:我們使用JDBC進(jìn)行操作數(shù)據(jù)庫(kù)時(shí),SQL語(yǔ)句基本都散落在各個(gè)JAVA類中,這樣有三個(gè)不足之處:第一,可讀性很差,不利于維護(hù)以及做性能調(diào)優(yōu)。第二,改動(dòng)Java代碼需要重新編譯、打包部署。第三,不利于取出SQL在數(shù)據(jù)庫(kù)客戶端執(zhí)行(取出后還得刪掉中間的Java代碼,編寫好的SQL語(yǔ)句寫好后還得通過+號(hào)在Java進(jìn)行拼湊)。解決問題:我們可以考慮不把SQL語(yǔ)句寫到Java代碼中,那么把SQL語(yǔ)句放到哪里呢?首先需要有一個(gè)統(tǒng)一存放的地方,我們可以將這些SQL語(yǔ)句統(tǒng)一集中放到配置文件或者數(shù)據(jù)庫(kù)里面(以key-value的格式存放)。然后通過SQL語(yǔ)句的key值去獲取對(duì)應(yīng)的SQL語(yǔ)句。既然我們將SQL語(yǔ)句都統(tǒng)一放在配置文件或者數(shù)據(jù)庫(kù)中,那么這里就涉及一個(gè)SQL語(yǔ)句的加載問題。3.3第三步優(yōu)化:傳入?yún)?shù)映射和動(dòng)態(tài)SQL問題描述:很多情況下,我們都可以通過在SQL語(yǔ)句中設(shè)置占位符來(lái)達(dá)到使用傳入?yún)?shù)的目的,這種方式本身就有一定局限性,它是按照一定順序傳入?yún)?shù)的,要與占位符一一匹配。但是,如果我們傳入的參數(shù)是不確定的(比如列表查詢,根據(jù)用戶填寫的查詢條件不同,傳入查詢的參數(shù)也是不同的,有時(shí)是一個(gè)參數(shù)、有時(shí)可能是三個(gè)參數(shù)),那么我們就得在后臺(tái)代碼中自己根據(jù)請(qǐng)求的傳入?yún)?shù)去拼湊相應(yīng)的SQL語(yǔ)句,這樣的話還是避免不了在Java代碼里面寫SQL語(yǔ)句的命運(yùn)。既然我們已經(jīng)把SQL語(yǔ)句統(tǒng)一存放在配置文件或者數(shù)據(jù)庫(kù)中了,怎么做到能夠根據(jù)前臺(tái)傳入?yún)?shù)的不同,動(dòng)態(tài)生成對(duì)應(yīng)的SQL語(yǔ)句呢?解決問題:第一,我們先解決這個(gè)動(dòng)態(tài)問題,按照我們正常的程序員思維是,通過if和else這類的判斷來(lái)進(jìn)行是最直觀的,這個(gè)時(shí)候我們想到了JSTL中的這樣的標(biāo)簽,那么,能不能將這類的標(biāo)簽引入到SQL語(yǔ)句中呢?假設(shè)可以,那么我們這里就需要一個(gè)專門的SQL解析器來(lái)解析這樣的SQL語(yǔ)句,但是,if判斷的變量來(lái)自于哪里呢?傳入的值本身是可變的,那么我們得為這個(gè)值定義一個(gè)不變的變量名稱,而且這個(gè)變量名稱必須和對(duì)應(yīng)的值要有對(duì)應(yīng)關(guān)系,可以通過這個(gè)變量名稱找到對(duì)應(yīng)的值,這個(gè)時(shí)候我們想到了key-value的Map。解析的時(shí)候根據(jù)變量名的具體值來(lái)判斷。假如前面可以判斷沒有問題,那么假如判斷的結(jié)果是true,那么就需要輸出的標(biāo)簽里面的SQL片段,但是怎么解決在標(biāo)簽里面使用變量名稱的問題呢?這里我們需要使用一種有別于SQL的語(yǔ)法來(lái)嵌入變量(比如使用#變量名#)。這樣,SQL語(yǔ)句經(jīng)過解析后就可以動(dòng)態(tài)的生成符合上下文的SQL語(yǔ)句。還有,怎么區(qū)分開占位符變量和非占位變量?有時(shí)候我們單單使用占位符是滿足不了的,占位符只能為查詢條件占位,SQL語(yǔ)句其他地方使用不了。這里我們可以使用#變量名#表示占位符變量,使用變量名表示非占位符變量。3.4第四步優(yōu)化:結(jié)果映射和結(jié)果緩存問題描述:執(zhí)行SQL語(yǔ)句、獲取執(zhí)行結(jié)果、對(duì)執(zhí)行結(jié)果進(jìn)行轉(zhuǎn)換處理、釋放相關(guān)資源是一整套下來(lái)的。假如是執(zhí)行查詢語(yǔ)句,那么執(zhí)行SQL語(yǔ)句后,返回的是一個(gè)ResultSet結(jié)果集,這個(gè)時(shí)候我們就需要將ResultSet對(duì)象的數(shù)據(jù)取出來(lái),不然等到釋放資源時(shí)就取不到這些結(jié)果信息了。我們從前面的優(yōu)化來(lái)看,以及將獲取連接、設(shè)置傳入?yún)?shù)、執(zhí)行SQL語(yǔ)句、釋放資源這些都封裝起來(lái)了,只剩下結(jié)果處理這塊還沒有進(jìn)行封裝,如果能封裝起來(lái),每個(gè)數(shù)據(jù)庫(kù)操作都不用自己寫那么一大堆Java代碼,直接調(diào)用一個(gè)封裝的方法就可以搞定了。解決問題:我們分析一下,一般對(duì)執(zhí)行結(jié)果的有哪些處理,有可能將結(jié)果不做任何處理就直接返回,也有可能將結(jié)果轉(zhuǎn)換成一個(gè)JavaBean對(duì)象返回、一個(gè)Map返回、一個(gè)List返回等`,結(jié)果處理可能是多種多樣的。從這里看,我們必須告訴SQL處理器兩點(diǎn):第一,需要返回什么類型的對(duì)象;第二,需要返回的對(duì)象的數(shù)據(jù)結(jié)構(gòu)怎么跟執(zhí)行的結(jié)果映射,這樣才能將具體的值copy到對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)上。接下來(lái),我們可以進(jìn)而考慮對(duì)SQL執(zhí)行結(jié)果的緩存來(lái)提升性能。緩存數(shù)據(jù)都是key-value的格式,那么這個(gè)key怎么來(lái)呢?怎么保證唯一呢?即使同一條SQL語(yǔ)句幾次訪問的過程中由于傳入?yún)?shù)的不同,得到的執(zhí)行SQL語(yǔ)句也是不同的。那么緩存起來(lái)的時(shí)候是多對(duì)。但是SQL語(yǔ)句和傳入?yún)?shù)兩部分合起來(lái)可以作為數(shù)據(jù)緩存的key值。3.5第五步優(yōu)化:解決重復(fù)SQL語(yǔ)句問題問題描述:由于我們將所有SQL語(yǔ)句都放到配置文件中,這個(gè)時(shí)候會(huì)遇到一個(gè)SQL重復(fù)的問題,幾個(gè)功能的SQL語(yǔ)句其實(shí)都差不多,有些可能是SELECT后面那段不同、有些可能是WHERE語(yǔ)句不同。有時(shí)候表結(jié)構(gòu)改了,那么我們就需要改多個(gè)地方,不利于維護(hù)。解決問題:當(dāng)我們的代碼程序出現(xiàn)重復(fù)代碼時(shí)怎么辦?將重復(fù)的代碼抽離出來(lái)成為獨(dú)立的一個(gè)類,然后在各個(gè)需要使用的地方進(jìn)行引用。對(duì)于SQL重復(fù)的問題,我們也可以采用這種方式,通過將SQL片段模塊化,將重復(fù)的SQL片段獨(dú)立成一個(gè)SQL塊,然后在各個(gè)SQL語(yǔ)句引用重復(fù)的SQL塊,這樣需要修改時(shí)只需要修改一處即可。4Mybaits有待改進(jìn)之處問題描述:Mybaits所有的數(shù)據(jù)庫(kù)操作都是基于SQL語(yǔ)句,導(dǎo)致什么樣的數(shù)據(jù)庫(kù)操作都要寫SQL語(yǔ)句。一個(gè)應(yīng)用系統(tǒng)要寫的SQL語(yǔ)句實(shí)在太多了。改進(jìn)方法:我們對(duì)數(shù)據(jù)庫(kù)進(jìn)行的操作大部分都是對(duì)表數(shù)據(jù)的增刪改查,很多都是對(duì)單表的數(shù)據(jù)進(jìn)行操作,由這點(diǎn)我們可以想到一個(gè)問題:?jiǎn)伪聿僮骺刹豢梢圆粚慡QL語(yǔ)句,通過JavaBean的默認(rèn)映射器生成對(duì)應(yīng)的SQL語(yǔ)句,比如:一個(gè)類UserInfo對(duì)應(yīng)于USER_INFO表,userId屬性對(duì)應(yīng)于USER_ID字段。這樣我們就可以通過反射可以獲取到對(duì)應(yīng)的表結(jié)構(gòu)了,拼湊成對(duì)應(yīng)的SQL語(yǔ)句顯然不是問題。5MyBatis框架整體設(shè)計(jì)MyBatis框架整體設(shè)計(jì)5.1接口層-和數(shù)據(jù)庫(kù)交互的方式MyBatis和數(shù)據(jù)庫(kù)的交互有兩種方式:使用傳統(tǒng)的MyBatis提供的API;使用Mapper接口;5.1.1使用傳統(tǒng)的MyBatis提供的API這是傳統(tǒng)的傳遞StatementId和查詢參數(shù)給SqlSession對(duì)象,使用SqlSession對(duì)象完成和數(shù)據(jù)庫(kù)的交互;MyBatis提供了非常方便和簡(jiǎn)單的API,供用戶實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的增刪改查數(shù)據(jù)操作,以及對(duì)數(shù)據(jù)庫(kù)連接信息和MyBatis自身配置信息的維護(hù)操作。傳統(tǒng)的MyBatis工作模式上述使用MyBatis的方法,是創(chuàng)建一個(gè)和數(shù)據(jù)庫(kù)打交道的SqlSession對(duì)象,然后根據(jù)StatementId和參數(shù)來(lái)操作數(shù)據(jù)庫(kù),這種方式固然很簡(jiǎn)單和實(shí)用,但是它不符合面向?qū)ο笳Z(yǔ)言的概念和面向接口編程的編程習(xí)慣。由于面向接口的編程是面向?qū)ο蟮拇筅厔?shì),MyBatis為了適應(yīng)這一趨勢(shì),增加了第二種使用MyBatis支持接口(Interface)調(diào)用方式。5.1.2使用Mapper接口MyBatis將配置文件中的每一個(gè)節(jié)點(diǎn)抽象為一個(gè)Mapper接口:這個(gè)接口中聲明的方法和節(jié)點(diǎn)中的節(jié)點(diǎn)項(xiàng)對(duì)應(yīng),即節(jié)點(diǎn)的id值為Mapper接口中的方法名稱,parameterType值表示Mapper對(duì)應(yīng)方法的入?yún)㈩愋?,而resultMap值則對(duì)應(yīng)了Mapper接口表示的返回值類型或者返回結(jié)果集的元素類型。Mapper接口和Mapper.xml配置文件之間的對(duì)應(yīng)關(guān)系根據(jù)MyBatis的配置規(guī)范配置好后,通過SqlSession.getMapper(XXXMapper.class)方法,MyBatis會(huì)根據(jù)相應(yīng)的接口聲明的方法信息,通過動(dòng)態(tài)代理機(jī)制生成一個(gè)Mapper實(shí)例,我們使用Mapper接口的某一個(gè)方法時(shí),MyBatis會(huì)根據(jù)這個(gè)方法的方法名和參數(shù)類型,確定StatementId,底層還是通過SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject);等等來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的操作,MyBatis引用Mapper接口這種調(diào)用方式,純粹是為了滿足面向接口編程的需要。(其實(shí)還有一個(gè)原因是在于,面向接口的編程,使得用戶在接口上可以使用注解來(lái)配置SQL語(yǔ)句,這樣就可以脫離XML配置文件,實(shí)現(xiàn)“0配置”)。5.2數(shù)據(jù)處理層數(shù)據(jù)處理層可以說是MyBatis的核心,從大的方面上講,它要完成兩個(gè)功能:通過傳入?yún)?shù)構(gòu)建動(dòng)態(tài)SQL語(yǔ)句;SQL語(yǔ)句的執(zhí)行以及封裝查詢結(jié)果集成List;5.2.1參數(shù)映射和動(dòng)態(tài)SQL語(yǔ)句生成動(dòng)態(tài)語(yǔ)句生成可以說是MyBatis框架非常優(yōu)雅的一個(gè)設(shè)計(jì),MyBatis通過傳入的參數(shù)值,使用Ognl來(lái)動(dòng)態(tài)地構(gòu)造SQL語(yǔ)句,使得MyBatis有很強(qiáng)的靈活性和擴(kuò)展性。參數(shù)映射指的是對(duì)于java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的轉(zhuǎn)換:這里有包括兩個(gè)過程:查詢階段,我們要將java類型的數(shù)據(jù),轉(zhuǎn)換成jdbc類型的數(shù)據(jù),通過preparedStatement.setXXX()來(lái)設(shè)值;另一個(gè)就是對(duì)resultset查詢結(jié)果集的jdbcType數(shù)據(jù)轉(zhuǎn)換成java數(shù)據(jù)類型。5.2.2SQL語(yǔ)句的執(zhí)行以及封裝查詢結(jié)果集成List###動(dòng)態(tài)SQL語(yǔ)句生成之后,MyBatis將執(zhí)行SQL語(yǔ)句,并將可能返回的結(jié)果集轉(zhuǎn)換成List列表。MyBatis在對(duì)結(jié)果集的處理中,支持結(jié)果集關(guān)系一對(duì)多和多對(duì)一的轉(zhuǎn)換,并且有兩種支持方式,一種為嵌套查詢語(yǔ)句的查詢,還有一種是嵌套結(jié)果集的查詢。搜索后端架構(gòu)師公眾號(hào)回復(fù)“架構(gòu)整潔”,送你一份驚喜禮包。5.3框架支撐層事務(wù)管理機(jī)制事務(wù)管理機(jī)制對(duì)于ORM框架而言是不可缺少的一部分,事務(wù)管理機(jī)制的質(zhì)量也是考量一個(gè)ORM框架是否優(yōu)秀的一個(gè)標(biāo)準(zhǔn)。連接池管理機(jī)制由于創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接所占用的資源比較大,對(duì)于數(shù)據(jù)吞吐量大和訪問量非常大的應(yīng)用而言,連接池的設(shè)計(jì)就顯得非常重要。緩存機(jī)制為了提高數(shù)據(jù)利用率和減小服務(wù)器和數(shù)據(jù)庫(kù)的壓力,MyBatis會(huì)對(duì)于一些查詢提供會(huì)話級(jí)別的數(shù)據(jù)緩存,會(huì)將對(duì)某一次查詢,放置到SqlSession中,在允許的時(shí)間間隔內(nèi),對(duì)于完全相同的查詢,MyBatis會(huì)直接將緩存結(jié)果返回給用戶,而不用再到數(shù)據(jù)庫(kù)中查找。SQL語(yǔ)句的配置方式傳統(tǒng)的MyBatis配置SQL語(yǔ)句方式就是使用XML文件進(jìn)行配置的,但是這種方式不能很好地支持面向接口編程的理念,為了支持面向接口的編程,MyBatis引入了Mapper接口的概念,面向接口的引入,對(duì)使用注解來(lái)配置SQL語(yǔ)句成為可能,用戶只需要在接口上添加必要的注解即可,不用再去配置XML文件了,但是,目前的MyBatis只是對(duì)注解配置SQL語(yǔ)句提供了有限的支持,某些高級(jí)功能還是要依賴XML配置文件配置SQL語(yǔ)句。5.4引導(dǎo)層引導(dǎo)層是配置和啟動(dòng)MyBatis配置信息的方式。MyBatis提供兩種方式來(lái)引導(dǎo)MyBatis:基于XML配置文件的方式和基于JavaAPI的方式。5.5主要構(gòu)件及其相互關(guān)系從MyBatis代碼實(shí)現(xiàn)的角度來(lái)看,MyBatis的主要的核心部件有以下幾個(gè):SqlSession:作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫(kù)交互的會(huì)話,完成必要數(shù)據(jù)庫(kù)增刪改查功能;Executor:MyBatis執(zhí)行器,是MyBatis調(diào)度的核心,負(fù)責(zé)SQL語(yǔ)句的生成和查詢緩存的維護(hù);StatementHandler:封裝了JDBCStatement操作,負(fù)責(zé)對(duì)JDBCstatement的操作,如設(shè)置參數(shù)、將Statement結(jié)果集轉(zhuǎn)換成List集合。ParameterHandler:負(fù)責(zé)對(duì)用戶傳遞的參數(shù)轉(zhuǎn)換成JDBCStatement所需要的參數(shù);ResultSetHandler:負(fù)責(zé)將JDBC返回的ResultSet結(jié)果集對(duì)象轉(zhuǎn)換成List類型的集合;TypeHandler:負(fù)責(zé)java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換;MappedStatement:MappedStatement維護(hù)了一條節(jié)點(diǎn)的封裝;SqlSource:負(fù)責(zé)根據(jù)用戶傳遞的parameterObject,動(dòng)態(tài)地生成SQL語(yǔ)句,將信息封裝到BoundSql對(duì)象中,并返回;BoundSql:表示動(dòng)態(tài)生成的SQL語(yǔ)句以及相應(yīng)的參數(shù)信息;Configuration:MyBatis所有的配置信息都維持在Configuration對(duì)象之中;它們的關(guān)系如下圖所示:MyBatis主要構(gòu)件關(guān)系如圖6SqlSession工作過程分析開啟一個(gè)數(shù)據(jù)庫(kù)訪問會(huì)話---創(chuàng)建SqlSession對(duì)象SqlSession
sqlSession
=
factory.openSession();MyBatis封裝了對(duì)數(shù)據(jù)庫(kù)的訪問,把對(duì)數(shù)據(jù)庫(kù)的會(huì)話和事務(wù)控制放到了SqlSession對(duì)象中為SqlSession傳遞一個(gè)配置的Sql語(yǔ)句的StatementId和參數(shù),然后返回結(jié)果:Listresult=sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml的StatementID,params是傳遞的查詢參數(shù)。讓我們來(lái)看一下sqlSession.selectList()方法的定義:publicListselectList(Stringstatement,Objectparameter){returnthis.selectList(statement,parameter,RowBounds.DEFAULT);}publicListselectList(Stringstatement,Objectparameter,RowBoundsrowBounds){try{//1.根據(jù)StatementId,在mybatis配置對(duì)象Configuration中查找和配置文件相對(duì)應(yīng)的MappedStatementMappedStatementms=configuration.getMappedStatement(statement);//2.將查詢?nèi)蝿?wù)委托給MyBatis的執(zhí)行器ExecutorListresult=executor.query(ms,wrapCollection(parameter),rowBounds,Executor.NO_RESULT_HANDLER);returnresult;}catch(Exceptione){throwExceptionFactory.wrapException("Errorqueryingdatabase.Cause:"+e,e);}finally{ErrorContext.instance().reset();}}MyBatis在初始化的時(shí)候,會(huì)將MyBatis的配置信息全部加載到內(nèi)存中,使用org.apache.ibatis.session.Configuration實(shí)例來(lái)維護(hù)。使用者可以使用sqlSession.getConfiguration()方法來(lái)獲取。MyBatis的配置文件中配置信息的組織格式和內(nèi)存中對(duì)象的組織格式幾乎完全對(duì)應(yīng)的。上述例子中的:<selectid="selectByMinSalary"resultMap="BaseResultMap"parameterType="java.util.Map">selectEMPLOYEE_ID,FIRST_NAME,LAST_NAME,EMAIL,SALARYfromLOUIS.EMPLOYEES<iftest="min_salary!=null">whereSALARY<#{min_salary,jdbcType=DECIMAL}if>select>加載到內(nèi)存中會(huì)生成一個(gè)對(duì)應(yīng)的MappedStatement對(duì)象,然后會(huì)以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",value為MappedStatement對(duì)象的形式維護(hù)到Configuration的一個(gè)Map中。當(dāng)以后需要使用的時(shí)候,只需要通過Id值來(lái)獲取就可以了。從上述的代碼中我們可以看到SqlSession的職能是:SqlSession根據(jù)StatementID,在mybatis配置對(duì)象Configuration中獲取到對(duì)應(yīng)的MappedStatement對(duì)象,然后調(diào)用mybatis執(zhí)行器來(lái)執(zhí)行具體的操作。MyBatis執(zhí)行器Executor根據(jù)SqlSession傳遞的參數(shù)執(zhí)行query()方法(由于代碼過長(zhǎng),讀者只需閱讀我注釋的地方即可):/***BaseExecutor類部分代碼**/publicListquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{//1.根據(jù)具體傳入的參數(shù),動(dòng)態(tài)地生成需要執(zhí)行的SQL語(yǔ)句,用BoundSql對(duì)象表示BoundSqlboundSql=ms.getBoundSql(parameter);//2.為當(dāng)前的查詢創(chuàng)建一個(gè)緩存KeyCacheKeykey=createCacheKey(ms,parameter,rowBounds,boundSql);returnquery(ms,parameter,rowBounds,resultHandler,key,boundSql);}@SuppressWarnings("unchecked")publicListquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeykey,BoundSqlboundSql)throwsSQLException{ErrorContext.instance().resource(ms.getResource()).activity("executingaquery").object(ms.getId());if(closed)thrownewExecutorException("Executorwasclosed.");if(queryStack==0&&ms.isFlushCacheRequired()){clearLocalCache();}Listlist;try{queryStack++;list=resultHandler==null?(List)localCache.getObject(key):null;if(list!=null){handleLocallyCachedOutputParameters(ms,key,parameter,boundSql);}else{//3.緩存中沒有值,直接從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)list=queryFromDatabase(ms,parameter,rowBounds,resultHandler,key,boundSql);}}finally{queryStack--;}if(queryStack==0){for(DeferredLoaddeferredLoad:deferredLoads){deferredLoad.load();}deferredLoads.clear();//issue#601if(configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT){clearLocalCache();//issue#482}}returnlist;}privateListqueryFromDatabase(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeykey,BoundSqlboundSql)throwsSQLException{Listlist;localCache.putObject(key,EXECUTION_PLACEHOLDER);try{//4.執(zhí)行查詢,返回List結(jié)果,然后將查詢的結(jié)果放入緩存之中l(wèi)ist=doQuery(ms,parameter,rowBounds,resultHandler,boundSql);}finally{localCache.removeObject(key);}localCache.putObject(key,list);if(ms.getStatementType()==StatementType.CALLABLE){localOutputParameterCache.putObject(key,parameter);}returnlist;}/****SimpleExecutor類的doQuery()方法實(shí)現(xiàn)**/publicListdoQuery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{Statementstmt=null;try{Configurationconfiguration=ms.getConfiguration();//5.根據(jù)既有的參數(shù),創(chuàng)建StatementHandler對(duì)象來(lái)執(zhí)行查詢操作StatementHandlerhandler=configuration.newStatementHandler(wrapper,ms,parameter,rowBounds,resultHandler,boundSql);//6.創(chuàng)建java.Sql.Statement對(duì)象,傳遞給StatementHandler對(duì)象stmt=prepareStatement(handler,ms.getStatementLog());//7.調(diào)用StatementHandler.query()方法,返回List結(jié)果集returnhandler.query(stmt,resultHandler);}finally{closeStatement(stmt);}}上述的Executor.query()方法幾經(jīng)轉(zhuǎn)折,最后會(huì)創(chuàng)建一個(gè)StatementHandler對(duì)象,然后將必要的參數(shù)傳遞給StatementHandler,使用StatementHandler來(lái)完成對(duì)數(shù)據(jù)庫(kù)的查詢,最終返回List結(jié)果集。從上面的代碼中我們可以看出,Executor的功能和作用是:根據(jù)傳遞的參數(shù),完成SQL語(yǔ)句的動(dòng)態(tài)解析,生成BoundSql對(duì)象,供StatementHandler使用;為查詢創(chuàng)建緩存,以提高性能;創(chuàng)建JDBC的Statement連接對(duì)象,傳遞給StatementHandler對(duì)象,返回List查詢結(jié)果;StatementHandler對(duì)象負(fù)責(zé)設(shè)置Statement對(duì)象中的查詢參數(shù)、處理JDBC返回的resultSet,將resultSet加工為L(zhǎng)ist集合返回:接著上面的Executor第六步,看一下:prepareStatement()方法的實(shí)現(xiàn):/****SimpleExecutor類的doQuery()方法實(shí)現(xiàn)**/publicListdoQuery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{Statementstmt=null;try{Configurationconfiguration=ms.getConfiguration();StatementHandlerhandler=configuration.newStatementHandler(wrapper,ms,parameter,rowBounds,resultHandler,boundSql);//1.準(zhǔn)備Statement對(duì)象,并設(shè)置Statement對(duì)象的參數(shù)stmt=prepareStatement(handler,ms.getStatementLog());//2.StatementHandler執(zhí)行query()方法,返回List結(jié)果returnhandler.query(stmt,resultHandler);}finally{closeStatement(stmt);}}privateStatementprepareStatement(StatementHandlerhandler,LogstatementLog)throwsSQLException{Statementstmt;Connectionconnection=getConnection(statementLog);stmt=handler.prepare(connection);//對(duì)創(chuàng)建的Statement對(duì)象設(shè)置參數(shù),即設(shè)置SQL語(yǔ)句中?設(shè)置為指定的參數(shù)handler.parameterize(stmt);returnstmt;}以上我們可以總結(jié)StatementHandler對(duì)象主要完成兩個(gè)工作:對(duì)于JDBC的PreparedStatement類型的對(duì)象,創(chuàng)建的過程中,我們使用的是SQL語(yǔ)句字符串會(huì)包含若干個(gè)?占位符,我們其后再對(duì)占位符進(jìn)行設(shè)值。
StatementHandler通過parameterize(statement)方法對(duì)Statement進(jìn)行設(shè)值;StatementHandler通過Listquery(Statementstatement,ResultHandlerresultHandler)方法來(lái)完成執(zhí)行Statement,和將Statement對(duì)象返回的resultSet封裝成List;StatementHandler的parameterize(statement)方法的實(shí)現(xiàn):/***StatementHandler類的parameterize(statement)方法實(shí)現(xiàn)*/publicvoidparameterize(Statementstatement)throwsSQLException{//使用ParameterHandler對(duì)象來(lái)完成對(duì)Statement的設(shè)值parameterHandler.setParameters((PreparedStatement)statement);}/****ParameterHandler類的setParameters(PreparedStatementps)實(shí)現(xiàn)*對(duì)某一個(gè)Statement進(jìn)行設(shè)置參數(shù)*/publicvoidsetParameters(PreparedStatementps)throwsSQLException{ErrorContext.instance().activity("settingparameters").object(mappedStatement.getParameterMap().getId());ListparameterMappings=boundSql.getParameterMappings();if(parameterMappings!=null){for(inti=0;i<parameterMappings.size();i++){ParameterMappingparameterMapping=parameterMappings.get(i);if(parameterMapping.getMode()!=ParameterMode.OUT){Objectvalue;StringpropertyName=parameterMapping.getProperty();if(boundSql.hasAdditionalParameter(propertyName)){//issue#448askfirstforadditionalparamsvalue=boundSql.getAdditionalParameter(propertyName);}elseif(parameterObject==null){value=null;}elseif(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){value=parameterObject;}else{MetaObjectmetaObject=configuration.newMetaObject(parameterObject);value=metaObject.getValue(propertyName);}//每一個(gè)Mapping都有一個(gè)TypeHandler,根據(jù)TypeHandler來(lái)對(duì)preparedStatement進(jìn)行設(shè)置參數(shù)TypeHandlertypeHandler=parameterMapping.getTypeHandler();JdbcTypejdbcType=parameterMapping.getJdbcType();if(value==null&&jdbcType==null)jdbcType=configuration.getJdbcTypeForNull();//設(shè)置參數(shù)typeHandler.setParameter(ps,i+1,value,jdbcType);}}}}從上述的代碼可以看到,StatementHandler的parameterize(Statement)方法調(diào)用了ParameterHandler的setParameters(statement)方法,
ParameterHandler的setParameters(Statement)方法負(fù)責(zé)根據(jù)我們輸入的參數(shù),對(duì)statement對(duì)象的?占位符處進(jìn)行賦值。StatementHandler的Listquery(Statementstatement,ResultHandlerresultHandler)方法的實(shí)現(xiàn):/***PreParedStatement類的query方法實(shí)現(xiàn)*/publicListquery(Statementstatement,ResultHandlerresultHandler)throwsSQLException{//1.調(diào)用preparedStatemnt。execute()方法,然后將resultSet交給ResultSetHandler處理PreparedStatementps=(PreparedStatement)statement;ps.execute();//2.使用ResultHandler來(lái)處理ResultSetreturnresultSetHandler.handleResultSets(ps);}從上述代碼我們可以看出,StatementHandler的Listquery(Statementstatement,ResultHandlerresultHandler)方法的實(shí)現(xiàn),是調(diào)用了ResultSetHandler的handleResultSets(Statement)方法。ResultSetHandler的handleResultSets(Statement)方法會(huì)將Statement語(yǔ)句執(zhí)行后生成的resultSet結(jié)果集轉(zhuǎn)換成List結(jié)果集:/***ResultSetHandler類的handleResultSets()方法實(shí)現(xiàn)**/publicListhandleResultSets(Statementstmt)throwsSQLException{finalListmultipleResults=newArrayList();intresultSetCount=0;ResultSetWrapperrsw=getFirstResultSet(stmt);ListresultMaps=mappedStatement.getResultMaps();intresultMapCount=resultMaps.size();validateResultMapsCount(rsw,resultMapCount);while(rsw!=null&&resultMapCount>resultSetCount){ResultMapresultMap=resultMaps.get(resultSetCount);//將resultSethandleResultSet(rsw,resultMap,multipleResults,null);rsw=getNextResultSet(stmt);cleanUpAfterHandlingResultSet();resultSetCount++;}String[]resultSets=mappedStatement.getResulSets();if(resultSets!=null){while(rsw!=null&&resultSetCount<resultSets.length){ResultMappingparentMapping=nextResultMaps.get(resultSets[resultSetCount]);if(parentMapping!=null){StringnestedResultMapId=parentMapping.getNestedResultMapId();ResultMapresultMap=configuration.getResultMap(nestedResultMapId);handleResultSet(rsw,resultMap,null,parentMapping);}rsw=getNextResultSet(stmt);cleanUpAfterHandlingResultSet();resultSetCount++;}}returncollapseSingleResultList(multipleResults);}7MyBatis初始化機(jī)制7.1MyBatis的初始化做了什么任何框架的初始化,無(wú)非是加載自己運(yùn)行時(shí)所需要的配置信息。MyBatis的配置信息,大概包含以下信息,其高層級(jí)結(jié)構(gòu)如下:搜索頂級(jí)架構(gòu)師公眾號(hào)回復(fù)“架構(gòu)”,送你一份驚喜禮包。MyBatis配置信息結(jié)構(gòu)圖MyBatis的上述配置信息會(huì)配置在XML配置文件中,那么,這些信息被加載進(jìn)入MyBatis內(nèi)部,MyBatis是怎樣維護(hù)的呢?MyBatis采用了一個(gè)非常直白和簡(jiǎn)單的方式---使用org.apache.ibatis.session.Configuration對(duì)象作為一個(gè)所有配置信息的容器,Configuration對(duì)象的組織結(jié)構(gòu)和XML配置文件的組織結(jié)構(gòu)幾乎完全一樣(當(dāng)然,Configuration對(duì)象的功能并不限于此,它還負(fù)責(zé)創(chuàng)建一些MyBatis內(nèi)部使用的對(duì)象,如Executor等,這將在后續(xù)的文章中討論)。如下圖所示:Configuration對(duì)象的組織結(jié)構(gòu)和XML配置文件的組織結(jié)構(gòu)幾乎完全一樣MyBatis根據(jù)初始化好Configuration信息,這時(shí)候用戶就可以使用MyBatis進(jìn)行數(shù)據(jù)庫(kù)操作了??梢赃@么說,MyBatis初始化的過程,就是創(chuàng)建Configuration對(duì)象的過程。MyBatis的初始化可以有兩種方式:基于XML配置文件:基于XML配置文件的方式是將MyBatis的所有配置信息放在XML文件中,MyBatis通過加載并XML配置文件,將配置文信息組裝成內(nèi)部的Configuration對(duì)象。基于JavaAPI:這種方式不使用XML配置文件,需要MyBatis使用者在Java代碼中,手動(dòng)創(chuàng)建Configuration對(duì)象,然后將配置參數(shù)set進(jìn)入Configuration對(duì)象中。接下來(lái)我們將通過基于XML配置文件方式的MyBatis初始化,深入探討MyBatis是如何通過配置文件構(gòu)建Configuration對(duì)象,并使用它。7.2基于XML配置文件創(chuàng)建Configuration對(duì)象##現(xiàn)在就從使用MyBatis的簡(jiǎn)單例子入手,深入分析一下MyBatis是怎樣完成初始化的,都初始化了什么??匆韵麓a:Stringresource="mybatis-config.xml";InputStreaminputStream=Resources.getResourceAsStream(resource);SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(inputStream);SqlSessionsqlSession=sqlSessionFactory.openSession();List
list
=
sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");有過MyBatis使用經(jīng)驗(yàn)的讀者會(huì)知道,上述語(yǔ)句的作用是執(zhí)行com.foo.bean.BlogMapper.queryAllBlogInfo定義的SQL語(yǔ)句,返回一個(gè)List結(jié)果集??偟膩?lái)說,上述代碼經(jīng)歷了mybatis初始化-->創(chuàng)建SqlSession-->執(zhí)行SQL語(yǔ)句返回結(jié)果三個(gè)過程。上述代碼的功能是根據(jù)配置文件mybatis-config.xml配置文件,創(chuàng)建SqlSessionFactory對(duì)象,然后產(chǎn)生SqlSession,執(zhí)行SQL語(yǔ)句。而mybatis的初始化就發(fā)生在第三句:SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(inputStream);
現(xiàn)在就讓我們看看第三句到底發(fā)生了什么。MyBatis初始化基本過程:SqlSessionFactoryBuilder根據(jù)傳入的數(shù)據(jù)流生成Configuration對(duì)象,然后根據(jù)Configuration對(duì)象創(chuàng)建默認(rèn)的SqlSessionFactory實(shí)例。初始化的基本過程如下序列圖所示:MyBatis初始化序列圖由上圖所示,mybatis初始化要經(jīng)過簡(jiǎn)單的以下幾步:調(diào)用SqlSessionFactoryBuilder對(duì)象的build(inputStream)方法;SqlSessionFactoryBuilder會(huì)根據(jù)輸入流inputStream等信息創(chuàng)建XMLConfigBuilder對(duì)象;SqlSessionFactoryBuilder調(diào)用XMLConfigBuilder對(duì)象的parse()方法;XMLConfigBuilder對(duì)象返回Configuration對(duì)象;SqlSessionFactoryBuilder根據(jù)Configuration對(duì)象創(chuàng)建一個(gè)DefaultSessionFactory對(duì)象;SqlSessionFactoryBuilder返回DefaultSessionFactory對(duì)象給Client,供Client使用。SqlSessionFactoryBuilder相關(guān)的代碼如下所示:publicSqlSessionFactorybuild(InputStreaminputStream){returnbuild(inputStream,null,null);}publicSqlSessionFactorybuild(InputStreaminputStream,Stringenvironment,Propertiesproperties){try{//2.創(chuàng)建XMLConfigBuilder對(duì)象用來(lái)解析XML配置文件,生成Configuration對(duì)象XMLConfigBuilderparser=newXMLConfigBuilder(inputStream,environment,properties);//3.將XML配置文件內(nèi)的信息解析成Java對(duì)象Configuration對(duì)象Configurationconfig=parser.parse();//4.根據(jù)Configuration對(duì)象創(chuàng)建出SqlSessionFactory對(duì)象returnbuild(config);}catch(Exceptione){throwExceptionFactory.wrapException("ErrorbuildingSqlSession.",e);}finally{ErrorContext.instance().reset();try{inputStream.close();}catch(IOExceptione){//Intentionallyignore.Preferpreviouserror.}}}//從此處可以看出,MyBatis內(nèi)部通過Configuration對(duì)象來(lái)創(chuàng)建SqlSessionFactory,用戶也可以自己通過API構(gòu)造好Configuration對(duì)象,調(diào)用此方法創(chuàng)SqlSessionFactorypublicSqlSessionFactorybuild(Configurationconfig){returnnewDefaultSqlSessionFactory(config);}上述的初始化過程中,涉及到了以下幾個(gè)對(duì)象:SqlSessionFactoryBuilder:SqlSessionFactory的構(gòu)造器,用于創(chuàng)建SqlSessionFactory,采用了Builder設(shè)計(jì)模式Configuration:該對(duì)象是mybatis-config.xml文件中所有mybatis配置信息SqlSessionFactory:SqlSession工廠類,以工廠形式創(chuàng)建SqlSession對(duì)象,采用了Factory工廠設(shè)計(jì)模式XMLConfigBuilder:負(fù)責(zé)將mybatis-config.xml配置文件解析成Configuration對(duì)象,共SqlSessonFactoryBuilder使用,創(chuàng)建SqlSessionFactory創(chuàng)建Configuration對(duì)象的過程:
接著上述的MyBatis初始化基本過程討論,當(dāng)SqlSessionFactoryBuilder執(zhí)行build()方法,調(diào)用了XMLConfigBuilder的parse()方法,然后返回了Configuration對(duì)象。那么parse()方法是如何處理XML文件,生成Configuration對(duì)象的呢?(1)XMLConfigBuilder會(huì)將XML配置文件的信息轉(zhuǎn)換為Document對(duì)象,而XML配置定義文件DTD轉(zhuǎn)換成XMLMapperEntityResolver對(duì)象,然后將二者封裝到XpathParser對(duì)象中,XpathParser的作用是提供根據(jù)Xpath表達(dá)式獲取基本的DOM節(jié)點(diǎn)Node信息的操作。如下圖所示:XpathParser組成結(jié)構(gòu)圖和生成圖(2)之后XMLConfigBuilder調(diào)用parse()方法:會(huì)從XPathParser中取出節(jié)點(diǎn)對(duì)應(yīng)的Node對(duì)象,然后解析此Node節(jié)點(diǎn)的子Node:properties,settings,typeAliases,typeHandlers,objectFactory,objectWrapperFactory,plugins,environments,databaseIdProvider,mappers:publicConfigurationparse(){if(parsed){thrownewBuilderException("EachXMLConfigBuildercanonlybeusedonce.");}parsed=true;//源碼中沒有這一句,只有parseConfiguration(parser.evalNode("/configuration"));//為了讓讀者看得更明晰,源碼拆分為以下兩句XNodeconfigurationNode=parser.evalNode("/configuration");parseConfiguration(configurationNode);returnconfiguration;}/***解析"/configuration"節(jié)點(diǎn)下的子節(jié)點(diǎn)信息,然后將解析的結(jié)果設(shè)置到Configuration對(duì)象中*/privatevoidparseConfiguration(XNoderoot){try{//1.首先處理properties節(jié)點(diǎn)propertiesElement(root.evalNode("properties"));//issue#117readpropertiesfirst//2.處理typeAliasestypeAliasesElement(root.evalNode("typeAliases"));//3.處理插件pluginElement(root.evalNode("plugins"));//4.處理objectFactoryobjectFactoryElement(root.evalNode("objectFactory"));//5.objectWrapperFactoryobjectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));//6.settingssettingsElement(root.evalNode("settings"));//7.處理environmentsenvironmentsElement(root.evalNode("environments"));//readitafterobjectFactoryandobjectWrapperFactoryissue#631//8.databasedatabaseIdProviderElement(root.evalNode("databaseIdProvider"));//9.typeHandlerstypeHandlerElement(root.evalNode("typeHandlers"));//10.mappersmapperElement(root.evalNode("mappers"));}catch(Exceptione){thrownewBuilderException("ErrorparsingSQLMapperConfiguration.Cause:"+e,e);}}注意:在上述代碼中,還有一個(gè)非常重要的地方,就是解析XML配置文件子節(jié)點(diǎn)的方法mapperElements(root.evalNode("mappers")),它將解析我們配置的Mapper.xml配置文件,Mapper配置文件可以說是MyBatis的核心,MyBatis的特性和理念都體現(xiàn)在此Mapper的配置和設(shè)計(jì)上。(3)然后將這些值解析出來(lái)設(shè)置到Configuration對(duì)象中:解析子節(jié)點(diǎn)的過程這里就不一一介紹了,用戶可以參照MyBatis源碼仔細(xì)揣摩,我們就看上述的environmentsElement(root.evalNode("environments"));方法是如何將environments的信息解析出來(lái),設(shè)置到Configuration對(duì)象中的:/***解析environments節(jié)點(diǎn),并將結(jié)果設(shè)置到Configuration對(duì)象中*注意:創(chuàng)建envronment時(shí),如果SqlSessionFactoryBuilder指定了特定的環(huán)境(即數(shù)據(jù)源);*則返回指定環(huán)境(數(shù)據(jù)源)的Environment對(duì)象,否則返回默認(rèn)的Environment對(duì)象;*這種方式實(shí)現(xiàn)了MyBatis可以連接多數(shù)據(jù)源*/privatevoidenvironmentsElement(XNodecontext)throwsException{if(context!=null){if(environment==null){environment=context.getStringAttribute("default");}for(XNodechild:context.getChildren()){Stringid=child.getStringAttribute("id");if(isSpecifiedEnvironment(id)){//1.創(chuàng)建事務(wù)工廠TransactionFactoryTransactionFactorytxFactory=transactionManagerElement(child.evalNode("transactionManager"));DataSourceFactorydsFactory=dataSourceElement(child.evalNode("dataSource"));//2.創(chuàng)建數(shù)據(jù)源DataSourceDataSourcedataSource=dsFactory.getDataSource();//3.構(gòu)造Environment對(duì)象Environment.BuilderenvironmentBuilder=newEnvironment.Builder(id).transactionFactory(txFactory).dataSource(dataSource);//4.將創(chuàng)建的Envronment對(duì)象設(shè)置到configuration對(duì)象中configuration.setEnvironment(environmentBuilder
溫馨提示
- 1. 本站所有資源如無(wú)特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 二零二五年度財(cái)務(wù)信息系統(tǒng)集成合同3篇
- 2024石子深加工技術(shù)研發(fā)與應(yīng)用合同3篇
- 2024玩具樂園設(shè)備采購(gòu)及租賃服務(wù)合同3篇
- 2024版影視作品版權(quán)轉(zhuǎn)讓與授權(quán)播放合同
- 2025年松樹造林項(xiàng)目采購(gòu)合同3篇
- 二零二五版船舶光租及船舶安全管理體系合同3篇
- 二零二五年度安置房項(xiàng)目公共設(shè)施維護(hù)合同3篇
- 2025年度淋浴房綠色環(huán)保材料采購(gòu)與安裝服務(wù)合同4篇
- 2025年度鋁材貿(mào)易結(jié)算與風(fēng)險(xiǎn)管理合同4篇
- 二零二五年度跨境電商進(jìn)口采購(gòu)合同3篇
- 領(lǐng)導(dǎo)溝通的藝術(shù)
- 發(fā)生用藥錯(cuò)誤應(yīng)急預(yù)案
- 南潯至臨安公路(南潯至練市段)公路工程環(huán)境影響報(bào)告
- 綠色貸款培訓(xùn)課件
- 大學(xué)生預(yù)征對(duì)象登記表(樣表)
- 主管部門審核意見三篇
- 初中數(shù)學(xué)校本教材(完整版)
- 父母教育方式對(duì)幼兒社會(huì)性發(fā)展影響的研究
- 新課標(biāo)人教版數(shù)學(xué)三年級(jí)上冊(cè)第八單元《分?jǐn)?shù)的初步認(rèn)識(shí)》教材解讀
- (人教版2019)數(shù)學(xué)必修第一冊(cè) 第三章 函數(shù)的概念與性質(zhì) 復(fù)習(xí)課件
- 重慶市銅梁區(qū)2024屆數(shù)學(xué)八上期末檢測(cè)試題含解析
評(píng)論
0/150
提交評(píng)論