前言
一些個人感受:不管分析什麼原始碼,如果我們能摸索出作者的心路歷程,跟著他的腳步一步一步往前走,這樣才能接近事實的真相,也能更平滑更有趣的學習到知識。跟福爾摩斯探案一樣,作者都經歷了些什麼,為什麼他要這樣去設計這樣去做,留給我們的只有無聲的程式碼和那一段孤獨的日子。
閱讀順序建議是從上往下閱讀,如果直接跳轉到某一節,沒有基於上面的分析推理的話可能會不容易理解。
一切的一切要從JDBC開始說起
先來一段JDBC程式碼回憶預熱一下,方便我們後面進入正題
Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); String sql = "SELECT id, first, last, age FROM student where id=?"; Statement stmt = conn.prepareStatement(sql); pre.setBigDecimal(1, 10000); ResultSet rs = stmt.executeQuery(); while(rs.next()){ int id = rs.getBigDecimal("id"); int age = rs.getInt("age"); } rs.close(); stmt.close(); conn.close();
關於jdbc為什麼要這樣去抽象我們先放到一邊,簡單提取出幾個關鍵物件:
Connection
Statement
ResultSet
一、mybatis抽象出來的關鍵物件
mybatis是怎樣一步一步演變出來的,其中設計思路是怎樣的,mybatis關鍵物件又是怎麼被抽象出來的?
1.Sql語句提取到xml檔案
眾所周知,mybatis的一大創新和亮點,是將sql語句寫到xml檔案
StringBuilder sql = new StringBuilder("SELECT * FROM BLOG WHERE state = 'ACTIVE'"); if (title != null) { sql.append("AND title like ?"); } if (author!=null&&author.name!=null){ sql.append("AND author_name like ?"); }
Mybatis將sql語句提出來放到xml裡,比上面java程式碼看起來可讀性操作性都強很多,而且sql會統一放在一個地方一起管理,等於將sql與程式碼進行了分離,後面從全域性去看sql、分析優化sql確實也會帶來便利。當然,也可以通過註解的形式把sql語句寫到java程式碼裡,這樣的目的和寫到xml一樣,也是為了把sql單獨提取出來。
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
然後配置檔案我們分為哪些呢,除了要執行的sql,即sql mapper外,我們還需要配置一些全域性的設定吧,例如資料來源等等
所以配置檔案我們分為兩類:
Sql語句的配置
BlogMapper.xml
<mapper namespace="BlogMapper"> <select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select> </mapper>
全域性的配置
config.xml
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC "> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="123"/> <property name="url" value="456"/> <property name="username" value="789"/> <property name="password" value="10"/> </dataSource> </environment> </environments> </configuration>
當然,以上通過xml檔案進行配置的都可用java程式碼進行配置
這裡environments我們不做過多分析,主要是把多環境的配置都寫在一起,但是不管配置多少個environment,最後也只會用 default屬性的那個,即只有一個在執行時生效
如果有多個資料來源,則需要多個config.xml配置檔案去配置對應的資料來源
那麼問題來了,上面兩類xml解析後放到哪裡,抽象出了哪些物件?
2.Configuration
將配置檔案統一解析到Configuration物件,從xml解析的內容先放在這,後面誰想用拿去用就行了,這裡還是很好理解
Configuration物件如何生成呢?
可以通過讀取config.xml檔案:
XMLConfigBuilder parser = new XMLConfigBuilder(reader); Configuration configuration=parser.parse();
當然,也可以通過java程式碼來初始化:
TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.setDatabaseId("mysql");
//基於java註解配置sql configuration.addMapper(IBlogMapper.class);
//基於mapper.xml配置sql Resource[] mapperLocations = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"); if (!isEmpty(mapperLocations)) { for (Resource mapperLocation : mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } } }
configuration物件為mybatis抽象出的第一個關鍵物件,configuration物件裡面長什麼樣,我們接著往下分析
2.1 SqlNode
首先我們從java解析xml開始,直接通過org.w3c.dom 來解析如下一段xml(mybatis的xml對映語句格式已經深入人心,我們這裡也先不去操心為什麼mybatis設計出sql語句在xml中寫成如下格式)
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
我們會得到父子關係如下的node集合(為了方便理解,我們忽略掉標籤之間換行\n節點,後文同樣也是省略掉):
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ =>Node(type:TEXT_NODE) <if test="title != null"> =>Node(type:Element) AND title like #{title} =>ChildNode(type:TEXT_NODE) </if> <if test="author != null and author.name != null"> =>Node(type:Element) AND author_name like #{author.name} =>ChildNode(type:TEXT_NODE) </if> </select>
我們得到父節點 <select>節點下一共有三個節點,然後兩個Element節點裡各有一個子節點
那麼該xml node我們應該如何存到記憶體裡呢,我們應該抽象成什麼物件呢?
這裡就引入了SqlNode物件,原始的org.w3c.dom 解析出來的Node物件已經滿足不了我們的需求,就算能滿足我們處理起來也很繞,所以我們要轉變成我們個性化的Node物件,方便去做判斷和sql的拼接等操作
所以在這裡每個xml node都會轉變成mybatis 的SqlNode,mybatis抽象出的SqlNode型別如下:
SqlNode | 說明 |
IfSqlNode | <if> 標籤生成的node,其test屬性需配合ognl使用 |
ChooseSqlNode | <choose> <when> <otherwise> 標籤生成的node |
ForEachSqlNode | <foreach> 標籤生成的node |
StaticTextSqlNode |
靜態文字內容,可以包含#{}佔位符
|
TextSqlNode |
也是動態的node,帶有${}佔位符的文字內容
|
VarDeclSqlNode | <bind> 標籤生成的node |
TrimSqlNode | <trim> 標籤生成的node |
SetSqlNode | 繼承自TrimSqlNode,<set> 標籤生成的node |
WhereSqlNode | 繼承自TrimSqlNode,<where> 標籤生成的node |
MixedSqlNode |
一種特殊的節點,不是由具體的sql標籤產生,相當於org.w3c.dom 的getChildNodes()返回的NodeList,即存放父節點的子節點集合 |
共 10 種,嚴格意義上來說只有 9 種, MixedSqlNode是一種特殊的節點,其本身並沒有什麼邏輯,只是在父節點存放其子節點的集合用
那麼上面xml轉換成mybatis SqlNode後長什麼樣呢?如下圖(為了方便理解,我們忽略掉標籤之間換行\n節點,後文同樣也是省略掉)
同org.w3c.dom 解析出來一樣, 一共三個節點,然後兩個Element節點裡各有一個子節點(不管一個節點的子節點有多少個,其子節點都會以集合形式統一放在MixSqlNode節點下)
StaticTextSqlNode
IfSqlNode
--StaticTextSqlNode(由MixedSqlNode進行一層包裝)
ifSqlNode
--StaticTextSqlNode(由MixedSqlNode進行一層包裝)
有同學肯定會說不對啊,少了一層MixedSqlNode
是的,只要父節點包含子節點,不論子節點有多少個,那麼子節點的集合統一都會放在MixedSqlNode節點下,是父子節點之間的媒介,為了方便理解我們這裡先省略掉它
ognl
只在<if>和<foreach>標籤的SqlNode中用到,例如if標籤裡常用到 test判斷,我們如何判斷對應的表示式呢,就是ognl的用武之地了
不清楚ognl的同學可以去搜尋一下該關鍵字,如下下劃線xml裡面的條件判斷都是通過ognl結合請求引數去執行出來結果
<if test="title != null">
<if test="author != null and author.name != null">
當把請求引數給到SqlNode時,通過引數和判斷表示式,再結合ognl就能得到boolean結果,這樣就可以去判斷是否要append當前節點的子節點的sql語句了
虛擬碼如下:
if (Ognl.getValue("title != null", parameterObject)) { sql.append("AND title like #{title}"); }
2.2 BoundSql
我們上面將xml裡的每段CRUD標籤解析成了對應的一批SqlNode
那麼執行時,通過請求引數我們需要提取出來最終到資料庫執行的jdbc statement,才能繼續將我們的流程往下走
#{} 佔位符
我們在mybatis xml中寫sql語句時,可以寫 #{} 和 ${} 佔位符,這是原始jdbc statment不支援的,這樣的書寫方式解決了我們之前sql語句引數要用 “?” 問號,然後statment賦值要注意順序的問題,引數一多眼睛就花了
mybatis將這個問題幫我們簡化了,可以在sql段裡面寫 #{} 佔位符,專案執行時 #{} 會被替換成 "?" 和對應排好序的引數集合
然後再去執行statement,虛擬碼如下:
Connection connection = transaction.getConnection();//從事務管理獲取connection PreparedStatement statement = connection.prepareStatement(sql);//準備statement for (int i = 0; i < parameterMappings.size(); i++) {//迴圈引數列表給statement賦值 Object value = requestObject.getValue(parameterMappings.get(i).getName());//通過反射拿到入參的屬性值 preparedStatement.setBigDecimal(i, new BigDecimal(value));//給statement賦值 } preparedStatement.execute();
幾個關鍵點:
1.prepareStatement 的 sql語句,即#{} 替換成 "?"的sql
2.#{} 替換成 "?" 後,排好序的引數列表
3.給statement賦值時,我們怎麼知道是 setInt 還是 setBigDecimal
這3個點,就是接下來要關注的,讓我們來看看mybatis是怎麼做的
Sql
如何通過SqlNode、請求引數 得到最終執行的sql?
其實上面說ognl的時候已經提到了,簡單理解就是由請求引數和條件表示式結合拼接出來,然後再把 "#{}" 替換成 "?" 即可
ParameterMapping
排好序的引數列表,給statement賦值使用
xml使用示例:
#{property,javaType=int,jdbcType=NUMERIC}
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
有如下一些關鍵的屬性:
property
即 #{xxx} 中的屬性名,是字串
javaType
通過 #{}佔位符中定義,如果沒有定義則找入參物件parameterType該屬性的型別
優先順序如下(由高到低):
1.xml配置檔案中定義的型別
2.入參物件該property屬性的java type
例如下面配置的 #{title},就是通過反射找 入參物件的title 屬性的java type
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
如果傳遞進來的入參是Map,那麼通過反射就找不到對應屬性的java type,這種情況下該屬性的 javaType 會設定成 Object.class
Map map=new HashMap();
map.put("title","123");
map.put("author",new Author(){{setName("tt");}});
session.select("com.tj.mybatis.IBlogMapper.findActiveBlogLike",map,null);
TypeHandler
#{property,javaType=int,jdbcType=NUMERIC}
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
優先順序如下(由高到低):
1.xml配置檔案中定義的型別
2.通過javaType去找對應的TypeHandler
該物件的作用就是解決給statement賦值時,讓我們知道是用ps.setInt(value) 還是 ps.setBigDecimal(value)
分為get 和 set:
給statement賦值時 通過java型別找jdbc型別
給java 物件賦值時 拿到資料庫查詢結果ResultSet後,是用哪個方法給java物件賦值rs.getInt("age"); 還是 rs.getBigDecimal("age");通過jdbc型別找java型別
UnknownTypeHandler
上面java type為Object.class時,例如入參是Map 找不到對應的屬性的java type,其對應的TypeHandler為UnknownTypeHandler
這種情況下,在給statement入參賦值時會再次根據獲取到的入參的值的型別去找TypeHandler
例如 title 屬性的值為 "123" 那麼再通過值"123"去找其對應的 TypeHandler,即StringTypeHandler
${} 佔位符
${} 和 #{} 這兩種佔位符的處理流程是不一樣的:
${}佔位符在執行時,會將sql替換成我們引數設定的sql段,有sql注入風險,且該sql段可能還包含#{}佔位符
例如:
select * from blog ${where}
可能會被替換成如下sql
select * from blog where title like #{title}
即替換內容為 "where title like #{title}",所以替換完後會再走一遍#{}佔位符的替換流程
如果xml中sql語句只包含 #{}佔位符,那麼通過請求引數,我們需要做的就是通過條件拼接sql(無sql注入風險),然後給statement引數賦值即可
如果xml中sql語句包含${}佔位符,那麼需要將${}佔位符進行替換,然後再進行上面#{}的流程,因為 ${} 可能包含 帶有#{}佔位符的語句替換進去
所以mybatis流程上是統一先處理${}佔位符,再處理#{}佔位符(SqlSource.getBoundSql 方法的流程),然後一個有sql注入風險一個無sql注入風險。
所以執行過程中,sqlNode最後變成了 statement所需要的兩大關鍵點:
1.sql(jdbc statement可直接使用的sql)
2.引數列表 ParameterMappings(排好序的,給statment賦值時直接按順序遍歷賦值),其又包含:屬性名property和TypeHandler
這就是我們的BoundSql物件,該物件包含上面兩個關鍵屬性
如下是大致的流程:
2.3 SqlSource
RawSqlSource 與 DynamicSqlSource
首先我們先分析一下如下兩段sql,在執行時執行時有什麼異同?
第一段sql:
<select id="selectBlog" resultType="Blog"> SELECT * FROM BLOG WHERE id = #{id} </select>
第二段sql:
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
第一段sql我們在執行時不需要根據傳遞進來的條件引數進行sql拼接,在專案啟動時就可以直接得到BoundSql的兩個關鍵屬性:
1.sql
SELECT * FROM BLOG WHERE id=?
2.引數列表:
id
在執行時,也根本不需要再做#{}標籤的替換,直接拿BoundSql和引數賦值給statment即可
而第二段sql我們在專案啟動時沒法提前得到BoundSql,只能在執行時通過傳遞進來的引數做判斷才能得到BoundSql。
總結:
第一段sql,靜態sql,執行時速度更快,專案載入時就能得到BoundSql
第二段sql,動態sql,執行時速度稍慢,執行時才能得到BoundSql
所以為了區分這兩種型別的SqlNode集合
靜態sql: RawSqlSource
當所有節點都是StaticTextSqlNode 或 MixedSqlNode ,就是RawSqlSource 靜態sql源(不需要依據請求引數來做判斷拼接sql,是固定的sql內容,如果有請求引數給statement賦值引數即可)
動態sql: DynamicSqlSource
只要包含除StaticTextSqlNode 和 MixedSqlNode 以外的其他8 種SqlNode型別 (sql中存在 ${}佔位符的是TextSqlNode),則都是DynamicSqlSource 動態sql源(需要根據請求引數做動態sql拼接)
所以不同的SqlSource得到BoundSql的速度不一樣,然後相同的是SqlSource下面都是放的SqlNode集合
有細心的同學看了肯定會說我漏了StaticSqlSource,其實StaticSqlSource是上面兩種SqlSource生成BoundSql的一個過渡產物,所以不需要單獨拎出來說明
2.4 LanguageDriver
mybatis除了可以通過xml寫sql外,也可以通過如下java 註解來寫sql,還可以通過freemarker、thymeleaf 等格式來寫書寫sql檔案
@Update({"<script>", "update Author", " <set>", " <if test='username != null'>username=#{username},</if>", " <if test='password != null'>password=#{password},</if>", " <if test='email != null'>email=#{email},</if>", " <if test='bio != null'>bio=#{bio}</if>", " </set>", "where id=#{id}", "</script>"}) void updateAuthorValues(Author author);
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
所以顧名思義,語言驅動 LanguageDriver的作用就是幹這個,將不同來源的sql解析成SqlSource物件,不過mybatis java註解的sql也是統一用的XmlLanguageDriver去解析的,這裡mybatis是為了方便擴充套件
2.5 MappedStatement
除了子節點SqlNode集合以外,<select> <update> <delete> 標籤也包含很多屬性,放到哪裡呢,新開一個父級的SqlNode嗎?而且從物件導向設計來說,這個Node跟下面的sql語句node區別還挺大的,至少跟上文那10種SqlNode差別挺大的,這裡新開一個物件用於存放父級標籤的屬性:MappedStatement
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </select>
sql語句的配置,每一段curd都會被解析成一個MappedStatement物件,可以通過id去與dao介面方法進行對應
這裡的中間產物我們就叫他MappedStatement,為什麼叫MappedStatement?
即mybatis最終生成jdbc statement的中間產物,mybatis做的事情就是 orm (object relational mapping),那麼最終生成statement的中間物就是MappedStatement
如下圖所示(右鍵新標籤頁開啟可檢視大圖)
注: 虛線箭頭表示此物件為通過某方法得到的返回值
例如:MappendStatement.getBoundSql(Object requestObject)得到的返回值為BoundSql物件
另外,每一段<select|insert|update|delete> 標籤,對應生成一個SqlSource、MappedStatement,1對1的關係
ParameterType
用於說明請求引數的java type,非必須,xml的<select|insert|update|delete>標籤中該屬性可以不寫,因為mybatis可以根據執行時傳遞進來的引數用反射判斷其型別
ResultMap ResultType
如官方文件所說,兩者只能用其中一個,不過不管用哪個,最終都是將資訊放在ResultMap,用於後面ResultSetHandler建立返回物件時使用
例如如下xml配置:
<select id="findActiveBlogLike" resultType="xxx.Blog">
生成的MappedStatement中,上面resultType會存放在ResultMap物件的type屬性裡
2.6 TransactionFactory
顧名思義其主要就是用於建立不同的Transaction物件,這裡涉及到mybatis的事務管理,關於事務管理下面內容我們會提到
3.StatementHandler
我們已經知道上面Configuration物件裡面有哪些內容,然後結合BoundSql就能夠將statement prepare 和 execute
如下虛擬碼示例:
Transaction transaction = configuration.getEnvironment().getTransactionFactory().newTransaction(dataSource, TransactionIsolationLevel.READ_COMMITTED, false); Connection connection = transaction.getConnection(); MappedStatement mappedStatement = configuration.getMappedStatement("findActiveBlogLike"); BoundSql boundSql = mappedStatement.getBoundSql(blog);
PreparedStatement statement = connection.prepareStatement(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
MetaObject metaObject = configuration.newMetaObject(parameterObject);//MetaObject是mybatis提供的能很方便使用反射的工具物件 if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); Object value = metaObject.getValue(parameterMapping.getProperty()); statement.setBigDecimal(i, new BigDecimal(value)); } } statement.execute(); ResultSet rs=statement.getResultSet(); while(rs.next()){ BigDecimal id = rs.getBigDecimal("id"); String title = rs.getString("title"); } rs.close(); statement.close(); connection.close();
我們知道,jdbc的statement有三種,每種執行起來有些區別:
Statement
Statement stm = conn.createStatement() return stm.execute(sql);
PreparedStatement
PreparedStatement pstm = conn.prepareStatement(sql); pstm.setString(1, "Hello"); return pstm.execute();
CallableStatement
CallableStatement cs = conn.prepareCall("{call xxx(?,?,?)}"); cs.setInt(1, 10); cs.setString(2, "Hello");
cs.registerOutParameter(3, Types.INTEGER);
return cs.execute();
所以這裡抽象出三個不同的Handler再部分結合模板方法去處理不同的statement,也挺好理解,最後不管什麼Statement都按如下模板來構建:
stmt = handler.prepare(transaction.getConnection(), transaction.getTimeout());
handler.parameterize(stmt);
區別是不同的hanlder裡的prepare()和parameterize()方法有些區別而已,例如StatementHandler的parameterize()方法裡程式碼為空,因為不支援引數設定
有了StatementHandler之後,我們的虛擬碼變成下面這樣:
StatementHandler handler = configuration.newStatementHandler(mappedStatement, parameterObject, boundSql); Statement stmt = handler.prepare(transaction.getConnection(), transaction.getTimeout()); handler.parameterize(stmt); handler.update(stmt); StatementHandler handler1 = configuration.newStatementHandler(mappedStatement, parameterObject, boundSql);
Statement stmt1 = handler.prepare(transaction.getConnection(), transaction.getTimeout());
handler1.parameterize(stmt);
handler1.query(stmt1, resultHandler);
transaction.getConnection().commit();
newStatementHandler() 建立的StatementHandler預設是PreparedStatementHandler,也可以在xml的<select|insert|update|delete>標籤中自己宣告型別
3.1 ParameterHandler
StatementHandler.parameterize()方法中的邏輯,交由ParameterHandler去執行,即迴圈BoundSql的ParameterMapping集合,結合TypeHandler給statement賦值
3.2 ResultSetHandler
顧名思義,StatementHandler執行完statement後,交由ResultSetHandler處理成xml中CRUD標籤ResultType ResultMap所宣告的物件
關於xml標籤中的ResultMap和ResultType,先回顧一下我們上面MappedStatement的內容:
不管是用ResultMap還是ResultType,最終都是將資訊放在ResultMap裡,ResultType會存放在ResultMap物件的type屬性裡
關於返回結果:
如果是 <select>標籤,這裡統一返回List<ResultType> 集合,如果結果只有一條,則直接list.get(0)就可以了
如果是 <insert|update|delete>標籤,則不會經過ResultSetHandler處理,statementHandler直接通過statement.getUpdateCount() 返回int值
1.建立返回ResultMap 、ResultType的物件 (ObjectFactory)
2.迴圈ResultSet每行,再迴圈每列,給物件屬性進行賦值 (TypeHandler)
3.如果是集合新增到集合再返回 (ResultHandler)
虛擬碼如下:
ResultSet rs = statement.getResultSet(); List<Object> list = objectFactory.create(List.class); while (rs.next()) { ResultSetMetaData metaData = rs.getMetaData(); final int columnCount = metaData.getColumnCount(); Object resultObject = objectFactory.create(resultMap.getType());//使用ObjectFactory例項化物件 MetaObject metaObject = configuration.newMetaObject(resultObject);//MetaObject是mybatis提供的能很方便使用反射的工具物件 for (int i = 1; i <= columnCount; i++) { String columnName = configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i); String property = metaObject.findProperty(columnName, configuration.isMapUnderscoreToCamelCase()); if (property != null && metaObject.hasSetter(property)) { Class<?> propertyType = metaObject.getSetterType(property); TypeHandler<?> typeHandler = getTypeHandler(propertyType, metaData.getColumnType(i));//通過屬性型別找對應的jdbc TypeHandler Object value = typeHandler.getResult(rs, columnName); metaObject.setValue(property, value); } } list.add(resultObject); }
ResultSetHandler配合ResultMap也支援巢狀查詢、子查詢,返回多結果集等,我們這裡就不細化了
ObjectFactory
顧名思義,物件工廠,產出物件用的,什麼物件呢,當然是查詢資料庫將結果對映到的java物件
用來建立ResultType(等同於ResultMap中的Type)等物件時使用,用反射建立物件(這裡可以做一些加工,比如建立完物件後給屬性賦值,但是這種情況不常見),
然後後面ResultSetHandler用TypeHandler去給新建立的物件屬性賦值
最後再用ResultHandler新增到返回集合裡
什麼場景適合我們自定義實現呢?
這裡的職責就是通過反射建立物件,一般情況下使用預設的DefaultObjectFactory就可以了;
如果想建立完物件給一些屬性初始化值,這裡可以做,但是可能會被後面資料庫查到的結果值覆蓋,使用下面的ResultHandler就可以實現
ResultHandler
為什麼需要ResultHandler?
區別於ResultSetHandler,ResultSet是jdbc返回的結果集,Result則理解為經過mybatis加工的結果
預設ResultSetHandler都會迴圈ResultSet然後通過DefaultResultHandler新增到集合,最後從ResultHandler取結果返回給呼叫方法(呼叫方法無返回型別限制)
上面虛擬碼中,如下幾句就是在DefaultResultHandler中執行:
List<Object> list = objectFactory.create(List.class);
list.add(resultObject);
只不過最後ResultSetHandler返回結果時自己呼叫了 defaultResultHandler.getResultList() 來進行返回。
如果想用自定義的ResultHandler:查詢方法必須是void型別,且入參有ResultHandler物件,然後結果集自己通過resultHandler來獲取,例如DefaultResultHandler.getResultList()
什麼場景適合我們自定義實現呢?
因為這裡的職責是建立返回集合List<ResultType>,並新增記錄行;所以我們可以對集合裡建立的物件進行一些統一的操作,例如給集合裡的物件某個欄位設定預設值
RowBounds
mybatis的記憶體分頁,在ResultSetHandler中使用,由外部方法層層傳遞進來,即通過RowBounds設定的引數對ResultSet進行 skip limit,只取想要頁數的記錄行
但是關鍵問題是基於記憶體的分頁,而不是物理分頁,所以基本上都不會用到
MetaObject
上面我們已經提到了,MetaObject是mybatis提供的方法使用反射的工具類,將物件Object扔進去,就可以很簡單的使用反射;自己專案中如果有需要也可以直接使用,很方便
MetaObject metaObject = configuration.newMetaObject(parameterObject);
metaObject.getValue("name");
需要注意的是此物件並不屬於我們StatementHandler,只是這裡用到比較多,所以我們就放到這裡一起講一下
4.Executor
熟悉mysql、mssql等關係型資料庫隔離級別的同學都知道,資料庫的隔離級別分為4類,由低到高:
1.Read Uncommitted 讀未提交
2.Read Committed 讀已提交
3.Repeatable Read 可重複讀
4.Serializable 序列
隔離級別越高則處理速度越慢,隔離級別越低則處理速度越快。
mysql預設隔離級別是Repeatable Read 可重複讀;即在同一個事務範圍內,同樣的查詢語句得到的結果一致。
mybatis的又一大亮點:同一個事務範圍內,基於記憶體實現可重複讀。直接在mybatis這裡就處理好了,都不用到資料庫,這樣減輕了資料庫壓力,且速度更快。
所以mybatis在這裡引入了快取和一些其他操作,而它的媒介就是Executor,是對StatementHandler再做一層封裝
Executor executor = configuration.newExecutor(transaction); executor.query(configuration.getMappedStatement("findActiveBlogLike"), parameterObject, rowBounds, Executor.NO_RESULT_HANDLER);
executor.commit()
Executor裡的虛擬碼:
List<E> list; CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);//通過關鍵物件建立唯一的快取key list = localCache.getObject(key);//通過快取key查快取 if (list == null) { StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); list = handler.<E>query(stmt, resultHandler); localCache.putObject(key, list);//存至快取 } return list;
就是我們上面所說的,對StatementHandler進行包裝,做一些邏輯封裝
然後Executor有哪幾種呢?主要還是知道這些物件是如何演變過來的,剩下的其實程式碼裡都能看的很明確了
Executor | 說明 |
BaseExecutor | 下面三種Executor的父類,基礎方法都在這裡,查詢方法實現了基於記憶體的一級快取 |
SimpleExecutor | 繼承自BaseExecutor,預設的Executor |
ResuseExecutor | 繼承自BaseExecutor,重用Statement,即同一個Executor內Statement不釋放重複使用 |
BatchExecutor | 繼承自BaseExecutor,針對增刪改的批處理,呼叫增刪改方法時,只是statement.addBatch(),最終還要通過呼叫commit方法觸發批處理 |
CachingExecutor | 在一級快取的基礎上增加二級快取,二級快取查不到的情況再去上面幾種Executor中進行查詢 |
Transaction
為什麼mybatis要抽象出Transaction事務物件,其實一方面是為了集中connection的管理,另一方面也是為了能夠適應趨勢解決事物發展過程中的問題,後面mybatis-spring中我們會詳細介紹。
spring關於事務的管理有:
DataSourceTransactionManager、PlatformTransactionManager等
mybatis這裡同樣也有自己的事務管理 Transaction介面的實現:JdbcTransaction 、SpringManagedTransaction等
相比spring表面看起來只是字尾少了個單詞 Manager而已
簡單點去理解,就是connection都是放在Transaction物件這裡進行管理,要運算元據庫連線都統一從這裡操作;
例如非託管的Transaction虛擬碼如下:
protected Connection connection;
public Connection getConnection(){
if (connection == null) {
connection = dataSource.getConnection();
}
return connection;
}
如果是受spring 託管的事務,則上面dataSource.getConnection() 變成 DataSourceUtils.getConnection();
一級快取
一級快取:預設開啟,且不能關閉,同一個Executor內(同一個事務)相同引數、sql語句讀到的結果是一樣的,都不用到資料庫,這樣減輕了資料庫壓力,且速度更快。
二級快取
CacheExecutor,可基於記憶體或第三方快取實現
要注意的是二級快取的key 是通過 mapper.xml 裡的namespace進行分組,例如:
<mapper namespace="UserMapper"> <cache eviction="FIFO" size="512" readOnly="true"/>
這樣所有該mapper <select>產生的cacheKey,都統一放在"UserMapper"這個namespace下彙總
mapper.xml裡面的<select|insert|update|delete> flushCache屬性設定為true時,會清空該namespace下所有cacheKey的快取
flushCache屬性在<select> 標籤中預設值為 false,在<insert|update|delete>標籤中預設值為 true。
然後如果其他mapper想共用同一個快取namespace,如下宣告就可以了
<mapper namespace="BlogMapper"> <cache-ref namespace="UserMapper"/>
5.SqlSession
mybatis為什麼要有session的概念? 上面使用Executor進行crud已經可以滿足我們絕大部分業務需求了,為什麼還要弄出個session的概念?
這裡主要還是為了強調會話的概念,由會話來控制事務的範圍,類似web 的session更方便使用者理解
那既然這樣,把上面Executor名字改成SqlSession不就行了?這樣其實也不好,因為對應的BatchExecutor、CachingExecutor改成BatchSqlSession、CachingSqlSession的話感覺有點混亂了,不符合session乾的事情
使用SqlSession後程式碼如下:
SqlSession session = sqlSessionFactory.openSession();//內部構造executor等物件 session.selectList("findActiveBlogLike",parameterObject);//內部使用Executor進行執行 session.commit();
session.close();
其實跟上面Executor的程式碼相比,也差不多,只不過SqlSessoin是通過factory工廠來建立,但是原理還是通過configuration建立transaction、executor等物件
Executor executor = configuration.newExecutor(transaction); executor.query(configuration.getMappedStatement("findActiveBlogLike"), parameterObject, rowBounds, Executor.NO_RESULT_HANDLER); executor.commit();
executor.close();
到這裡可以這樣理解,SqlSession就是為了更方便理解和使用而產生的物件,其方法本質還是交由Executor去執行。
到目前為止整體的架構如下(右鍵新標籤頁開啟可檢視大圖)
SqlSessionFactory
SqlSession的工廠類,需要的引數主要就是Configuration物件,其實意思很明確了,就是SqlSession需要使用Configuration物件,建立SqlSession程式碼如下
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession session = sqlSessionFactory.openSession();
不過configuration的構建其實還是挺麻煩的,上面Configuration已經提到,然後後面mybatis-spring有提供SqlSessionFactoryBean(包含Configuration的構建)方便我們更快捷的構建SqlSessionFactory
6.MapperProxy
熟悉mybatis的朋友都知道xml中每段<select|insert|update|delete>與dao介面方法是一對一的,其實早在ibatis的年代是沒有將兩者關聯起來的
java.lang.reflect.Proxy
那麼實現這一功能的核心是什麼呢,就是java的Proxy,通過session.getMapper(xxx.class)方法每次都會給介面生成一個代理Proxy的實現
實現後的效果:
try (SqlSession session = sqlSessionFactory.openSession()) {
IBlogMapper mapper = session.getMapper(IBlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
這裡我們就不分析Proxy的原理了,還是不明白的同學可以百度搜尋瞭解一下,如下是mybatis中使用proxy的程式碼:
DefaultSqlSession:
public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }
經由Configuration和MapperRegistry、MapperProxyFactory,最終執行返回:
protected T newInstance(MapperProxy<T> mapperProxy) { MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
不過需要注意的是getMapper(xxx.class)的使用前提的addMapper(xxx.class);否則不會生成代理;
addMapper可以由如下兩種形式觸發:
1.configuration.addMapper(xxx.Class);//基於java註解形式
2.xmlMapperBuilder.parse();//基於mapper.xml配置,詳細程式碼如下
Configuration configuration = new Configuration(environment);
Resource[] mapperLocations = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"); if (!isEmpty(mapperLocations)) { for (Resource mapperLocation : mapperLocations) { ... try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); ... } }
後面結合mybatis-spring時使用SqlSessionFactoryBean時就有幫我們實現了我們上面這段程式碼
MapperMethod
MapperProxy最後執行方法時,都會交給MapperMethod去執行,介面的每個方法method都會生成一個對應的MapperMethod去執行
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ... final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
然後只要在MapperMethod裡呼叫SqlSession對應的方法就算完成了:
public Object execute(SqlSession sqlSession, Object[] args) { Object result; //通過介面方法名找到對應的MappedStatement,判斷MappedStatement的標籤型別是其中哪種<select|insert|update|delete> switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); //呼叫對應的sqlSesion方法,傳遞MappedStatement id和請求引數,這裡的command.getName即MappedStatement的id(字首會自動加名稱空間來區分唯一) result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
其中就是select型別的方法複雜些,需要判斷介面裡的引數來去呼叫對應的SqlSession方法。
簡單點理解,就是呼叫mapper介面的方法,最後會被代理實現為呼叫對應的 sqlSession.select() 或 sqlSession.insert() 等對應的方法。
流程圖如下(右鍵新標籤頁開啟可檢視大圖)
最後呼叫的這個sqlSession從哪來?
在sqlSession.getMapper(xxx.class)時,會將sqlSession存到代理MapperProxy的屬性,然後MapperProxy呼叫MapperMethod時,會傳遞給MapperMethod去使用,即
//通過Proxy為介面生成並返回代理實現類MapperProxy,並將當前sqlSession存至代理實現類MapperProxy的屬性 IBlogMapper mapper = session.getMapper(IBlogMapper.class); //呼叫具體方法時,MapperProxy會呼叫MapperMethod來判斷執行對應的sqlSession.select 或 insert等方法,且此sqlSession就是上面生成代理類的sqlSession,是同一個 Blog blog = mapper.selectBlog(101);
如果是通過SqlSessionTemplate(後面mybatis-spring內容).getMapper(),則後面呼叫的sqlSession就是SqlSessionTemplate物件
然後這裡還有一點小細節,我們可以在生成代理實現類MapperProxy時,就可以遍歷介面的方法來提前生成好所有的MapperMethod【餓漢】,但是其實mybatis是在具體呼叫介面方法時,才生成對應的MapperMethod並快取到記憶體【懶漢】 ;具體利弊我們這裡就不做分析了。
7.Mybatis的外掛
首先我們為什麼需要外掛,哪裡需要用到外掛?其本質也是通過Proxy做一層代理
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
Interceptor示例:
public class XXXInterceptor implements Interceptor { public Object plugin(Object target) { return Plugin.wrap(target, this); } public Object intercept(Invocation invocation) { } }
Plugin程式碼:
public class Plugin implements InvocationHandler { public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } }
我們上面已經接觸了很多使用Proxy的場景了,這裡又是熟悉的配方,熟悉的味道。
一共有四個地方可以使用外掛,即可以被代理,當然被代理物件的所有方法都可以被攔截:
Executor
StatementHandler
ParameterHandler
ResultSetHandler
mybatis比較經典的外掛使用還是 pagehelper ,然後關於 外掛攔截的使用規範 在 pagehelper官方文件 中也講的很透徹很詳細,我相信在弄懂了本文mybatis原理後再去看 pagehelper這類外掛原始碼也會比較容易懂
8.Mybatis的擴充套件
由於我們基本上每個表都要用到一些增刪改查方法,然後我們生成程式碼時,總是會生成一堆類似的程式碼,xml檔案、mapper介面中存在大量相似程式碼,有沒有辦法把這些程式碼抽出來?
這時候mybatis-plus就出現了,其原理其實就是在mybatis 構建Configuration物件時做了加工,幫我們把增刪改查的MappedStatement新增進去;當然mybatis-plus還包含很多其它便捷的功能,但是也是也是基於mybatis做擴充套件。
還是那句話,我們把mybatis原理分析清楚了,這塊也就更容易去理解了,感興趣的同學可以從mybatis-plus的MybatisSqlSessionFactoryBean為源頭進去看
二、mybatis-spring抽象出來的關鍵物件
我們要知道mybatis 和 mybatis-spring是分開的兩個專案,然後又可以無縫的結合起來進行使用,但是為了便於我們理解,所以我們是分開進行分析,這樣更有利於吸收
與spring結合之前我們必須得熟悉一下spring的資料訪問與實務管理
1.事務管理的發展史
其實spring關於資料訪問、事務管理已經做得很好了,但是其中的發展史是怎樣的,對於理解mybatis的事務管理非常重要
我們簡單概括一下關於事務的發展過程中的幾個典型問題,儘量能夠讓大家回顧一下發展過程:
1.區域性事務的管理繫結了具體的資料訪問方式
問題描述:即connection-passing問題,不同方法想要共用事務需要在方法間傳遞connection,如果使用jdbc則傳遞connection物件,如果使用hibernate則需要傳遞session或transaction物件,不同的資料訪問形式要用不同的api來控制區域性事務,這樣我們的方法就業務層就沒辦法和資料訪問解耦
解決方法:connection繫結到執行緒ThreadLocal,在業務開始方法獲取連線,業務結束方法提交、釋放連線
2.事務管理程式碼與業務邏輯程式碼相互混雜
問題描述:上面問題1雖然解決了方法間傳遞資料庫連線的問題,但是事務的管理還是在業務程式碼裡,且需要合理控制,否則也會有問題
解決方法:面向切面程式設計,事務的切面管理(spring @Transactional)
如果還是不是很理解的朋友, 推薦去看一下《spring 揭密》一書裡的資料訪問和事務管理相關章節,增加這一塊的感知和認識,會有助於平滑的理解mybatis-spring的事務管理
2.Spring 之 DataSourceUtils、@Transactional
使用spring事務,註冊相關的bean:
@Bean public DataSourceTransactionManager transactionManager() { DataSourceTransactionManager dstm = new DataSourceTransactionManager(); dstm.setDataSource(dataSource); return dstm; } @Bean public BasicDataSource dataSource() { BasicDataSource bds = new BasicDataSource(); bds.setDriverClassName(""); bds.setUrl(""); bds.setUsername(""); bds.setPassword(""); return bds; }
具體的使用,注意TransactionManager和DataSourceUtils裡使用的dataSource是同一個,不然事務不生效:
@Transactional public void methodA(){ //簡單理解就是從ThreadLocal獲取資料庫連線,如果沒有就從DataSource獲取後set到ThreadLocal Connection connection = DataSourceUtils.getConnection(dataSource); PreparedStatement statement = connection.prepareStatement("insert into blog xxx"); statement.executeUpdate(); methodB(); }//@Transactional切面after:當前ThreadLocal的connection自動commit,並release DataSourceUtils ThreadLocal中的connection public void methodB(){ Connection connection = DataSourceUtils.getConnection(dataSource); PreparedStatement statement = connection.prepareStatement("insert into log xxx"); statement.executeUpdate(); }
如果方法不在@Transactional事務控制範圍內
需要注意的是如果方法不在@Transactional事務控制範圍內,通過DataSourceUtils.getConnection還是會存在ThreadLocal,只不過ThreadLocal中的connection就需要我們手動去 commit和release,當然DataSourceUtils有方法供我們呼叫。
DataSourceUtils中的虛擬碼:
private final ThreadLocal<Connection> tlConnection = new ThreadLocal<Connection>(); public static Connection getConnection(DataSource dataSource){ if (tlConnection.get() == null) { tlConnection.set(dataSource.getConnection()); } return tlConnection.get(); } public static void releaseConnection(){ tlConnection.get().close(); tlConnection.set(null); }
@Transactional切面實現的虛擬碼,需要結合TransactionManager和DataSourceUtils來使用,這裡簡化如下:
@After public void after(JoinPoint joinPoint){ commitConnection(); DataSourceUtils.releaseConnection(); }
結合spring事務時,connection資料庫連線線上程中的生命週期如下,即隨著事務開始而開始,隨時事務結束而結束
要注意ThreadLocal中set Connection是在業務程式碼中第一次獲取connection時,而不是@Transactional切面的before方法,在必須時才去獲取資料庫連線,而不是提前佔用
使用spring的資料訪問和事務管理就解決了我們上面所提到的兩個問題:
1.區域性事務的管理繫結了具體的資料訪問方式
2.事務管理程式碼與業務邏輯程式碼相互混雜
其實mybatis專案一直抽象到SqlSession,都沒有解決事務管理髮展的那兩個問題
多個方法如果想要共用SqlSession需要通過引數傳遞,且事務的提交也要我們自己寫在業務程式碼裡,如下:
public void methodA(){ SqlSession session = sqlSessionFactory.openSession(); session.insert("insertBlog",xxx); methodB(session); } public void methodB(SqlSesion session){ session.insert("insertUser",xxx); session.commit(); session.close(); }
3.SpringManagedTransaction
我們上文已經知道mybatis的Transaction物件是用來獲取、操作connection,但是也僅限於單個Executor、SqlSession內部,沒有放到執行緒ThreadLocal裡去,要想共用同一個connection事務,還是必須引數傳遞SqlSession或者Connection物件(即上面的問題1),如何解決?我們把Transaction裡的connection放到ThreadLocal不就解決了嗎?
那我們直接把Transaction物件裡的getConnection方法改一下不就行了
private final ThreadLocal<Connection> tlConnection = new ThreadLocal<Connection>(); public Connection getConnection(){ if (tlConnection.get() == null) { tlConnection.set(this.dataSource.getConnection()); } return tlConnection.get(); }
發現是不是跟DataSourceUtils的getConnection方法一模一樣,所以結合spring的資料訪問的話,可以精簡成:
public Connection getConnection(){ return DataSourceUtils.getConnection(this.dataSource); }
上面這段虛擬碼其實就是SpringManagedTransaction所幹的事情
4.SqlSessionUtils
然後我們結合@Transactional使用,我們來看看程式碼:
@Transactional public void methodA(){ TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
Transaction transaction = transactionFactory.newTransaction(dataSource);
Connection connection = transaction.getConnection();
PreparedStatement statement = connection.prepareStatement("insert into blog xxx");
statement.executeUpdate();
methodB(transaction);
}
public void methodB(Transaction transaction){
Connection connection = transaction.getConnection();
PreparedStatement statement = connection.prepareStatement("insert into user xxx");
statement.executeUpdate();
}
上面程式碼解決了問題2,但是沒解決問題1,是不用傳遞connection了,但是現在又要傳遞transaction。
類似connection,我們建立一個TransactionUtils工具類將transaction也繫結到ThreadLocal不就解決問題了?
@Transactional public void methodA(){ Connection connection = TransactionUtils.getTransaction().getConnection(); PreparedStatement statement = connection.prepareStatement("insert into blog xxx"); statement.executeUpdate(); methodB(transaction); } public void methodB(){ Connection connection = TransactionUtils.getTransaction().getConnection(); PreparedStatement statement = connection.prepareStatement("insert into user xxx"); statement.executeUpdate(); }
TransactionUtils的虛擬碼:
private final ThreadLocal<Transaction> tlTransaction = new ThreadLocal<Transaction>(); public static Transaction getTransaction(){ if (tlTransaction.get() == null) { TransactionFactory transactionFactory = new SpringManagedTransactionFactory(); Transaction transaction = transactionFactory.newTransaction(dataSource); tlTransaction.set(transaction); } return tlTransaction.get(); } public static void releaseTransaction(){ tlTransaction.get().connection.close(); tlTransaction.set(null); }
問題並沒有結束,我們要用的是mybatis的SqlSession,你這樣不是又回到原始的jdbc了,行我們繼續改,同樣類似DataSourceUtils我們再建個SqlSessionUtils行了吧:
@Transactional public void methodA(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory); sqlSession.insert("insertBlog",xxx); methodB(); } public void methodB(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory); sqlSession.insert("insertUser",xxx); }
SqlSessionUtils裡的虛擬碼:
private final ThreadLocal<SqlSession> tlSqlSession = new ThreadLocal<SqlSession>(); public static SqlSession getSqlSession(SqlSessionFactory factory){ if (tlSqlSession.get() == null) { SqlSession sqlSession = factory.openSession(); tlSqlSession.set(sqlSession); } return tlSqlSession.get(); }
現在SqlSession裡的connection已經通過SpringManagedTransaction打通spring的DataSourceUtils存到ThreadLocal,且@Transactional註解切面after會自動connection.commit(); 且釋放ThreadLocal資源(SqlSession)
但是還有一個問題:
同一個spring事務我們是使用相同的SqlSession了,但是我們想要的是@Transactional註解切面after自動實現sqlSession.commit() 而不是 connection.commit();其實SqlSession.commit()主要也是實現connection.commit(),這個確實是一點小瑕疵,但是確實是不影響使用。
這樣SqlSession的生命週期就實現了類似spring事務裡Connection的生命週期,且同connection一樣,ThreadLocal中set SqlSession是在業務程式碼中第一次獲取SqlSession時,而不是@Transactional切面的before方法,在必須時才去獲取,而不是提前獲取資源。
需要注意的是原始碼中SqlSessionUtils不是直接將SqlSession存在ThreadLocal,而是和spring的DataSourceUtils一樣,通過spring的TransactionSynchronizationManager來儲存到ThreadLocal,這裡為了便於理解我們直接進行了簡化。
如果不使用@Transactional註解進行事務管理的話怎麼使用SqlSessionUtils
SqlSession依然會幫我們存到ThreadLocal,不過同DataSourceUtils一樣就需要我們手動commit和release;因為沒人幫我們幹這個事情了,需要我們自己處理。當然SqlSessionUtils有提供方法供我們自己呼叫。
例如下面程式碼,如果這樣寫是不是就有問題了?就沒人幫我們commit和close connection了!
public void methodC(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory); sqlSession.insert("insertXXX",xxx); }
需要改成如下格式:
public void methodC(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory); sqlSession.insert("insertXXX",xxx); sqlSession.commit(); SqlSessionUtils.closeSqlSession(sqlSession,sqlSessionFactory); }
這下問題又麻煩了:
1.methodC可能會被其他方法受spring事務控制的方法呼叫,這樣其也會被納入spring事務範圍管理,不需要自己提交connection。
例如如果被上面methodA方法內部呼叫,@Transactional切面after會在methodA的所有程式碼(當然包括methodC的程式碼)執行完後自動提交connection
2.如果直接呼叫methodC,其本身又不在spring事務管理範圍,需要自己提交connection。
我們有沒有辦法判斷當前方法是否在@Transactional事務範圍內,如果在事務範圍內,就不處理,交由事務去提交;如果不在事務範圍內,就自己提交?
5.SqlSessionTemplate
上述問題我們做一下判斷,虛擬碼如下:
public void methodC(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); sqlSession.insert("insertXXX",xxx); if(isSqlSessionTransactional(sqlSession,sqlSessionFactory)){//判斷當前sqlSession是否在spring @Transactional事務管理範圍內 //donothing }else{ sqlSession.commit(); sqlSession.close(); } }
如何判斷當前sqlSession是否在spring @Transactional事務管理範圍內呢?如果感興趣的話可以直接去看一下原始碼,我們這裡就不囉嗦了
然後上面這段判斷程式碼我們不可能每個方法裡都寫一遍吧,有沒有辦法提取出來,我們就不繞彎子了,直接看優雅的SqlSessionTemplate:
public void methodC(){ SqlSessionTemplate sqlSessionTemplate=new SqlSessionTemplate(sqlSessionFactory); sqlSessionTemplate.insert("insertXXX",xxx); }
又是基於Proxy代理,在執行 SqlSession方法時,都交由代理去處理,SqlSessionTemplate的虛擬碼:
public class SqlSessionTemplate implements SqlSession, DisposableBean { public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ... this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); } public int insert(String statement, Object parameter) { return this.sqlSessionProxy.insert(statement, parameter); } private class SqlSessionInterceptor implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory); Object result = method.invoke(sqlSession, args);//執行sqlSession對應的方法 if(isSqlSessionTransactional(sqlSession,sqlSessionFactory)){//判斷當前sqlSession是否在spring @Transactional事務管理範圍內 //donothing }else{ sqlSession.commit(); sqlSession.close(); } } } }
終於,經歷了這麼多,mybatis-spring終於能夠與spring的事務管理比較完美的融合了?
問題仍然還沒結束,我們目前的操作也僅限於SqlSession的方法操作,我們上面基於Mapper介面的操作呢,回顧我們上面MapperProxy、MapperMethod,MapperMethod是呼叫SqlSession相應的方法,怎麼才能對接上SqlSessionTemplate
那還不簡單:
SqlSessionTemplate sqlSessionTemplate =new SqlSessionTemplate(sqlSessionFactory); IBlogMapper blogMapper = sqlSessionTemplate.getMapper(IBlogMapper.class); blogMapper.selectBlog(101);
6.MapperScannerConfigurer
現在我們結合mybatis-spring來使用SqlSession已經優雅了很多,我們也可以基於MapperProxy來實現上面的MethodA、MethodB的程式碼,這樣就省去了字串硬編碼,這種方式會更好:
@Transactional public void methodA(){ SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); IBlogMapper blogMapper = sqlSession.getMapper(IBlogMapper.class); blogMapper.insertBlog(xxx); methodB(); } public void methodB(){ SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); IUserMapper userMapper = sqlSession.getMapper(IUser.class); userMapper.insertUser(xxx); }
我們知道SqlSessionTemplate是基於proxy代理形式實現了對應的功能,那麼我們在結合spring使用的時候,能否把這個代理註冊成spring的bean呢,就是把sqlSession.getMapper(xxx.class)註冊成spring的bean,這樣我們就能夠使用如下@Autowired這樣更優雅的編碼:
@Autowired IBlogMapper blogMapper; @Autowired IUserMapper userMapper; @Transactional public void methodA(){ blogMapper.insertBlog(xxx); methodB(); } public void methodB(){ userMapper.insertUser(xxx); }
怎樣註冊spring bean呢,我們以IBlogMapper介面舉例:
public interface IBlogMapper { List<Blog> findActiveBlogLike(Map map); }
手動註冊實現類:
public class BlogMapper implements IBlogMapper { @Autowired SqlSessionFactory sqlSessionFactory; @Override public List<Blog> findActiveBlogLike(Map map) { SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); List<Blog> list = sqlSessionTemplate.selectList("findActiveBlogLike",map); return list; } }
不對啊,這裡沒有用到MapperProxy代理實現啊,而是自己手動去判斷和對映介面需要使用sqlsession的哪個方法了,完全沒MapperProxy和MapperMethod的事情啊?這肯定不是我們想要的!
Spring 之 BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor
要想給spring動態的註冊bean,這就又到了spring bean的生命週期的知識了,我們這裡就直接看mybatis-spring使用的什麼了,就不囉嗦spring bean生命週期了
@Bean MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("xxx"); msc.setAnnotationClass(Mapper.class);//可以設定只註冊新增了mybatis @Mapper註解的介面 msc.setSqlSessionFactoryBeanName("sqlSessionFactory"); return msc; }
MapperScannerConfigurer的實現:
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor ... { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); ... scanner.scan( StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
...
ClassPathMapperScanner的實現:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); ... processBeanDefinitions(beanDefinitions); ... return beanDefinitions; } private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class; private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); String beanClassName = definition.getBeanClassName(); definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59 definition.setBeanClass(this.mapperFactoryBeanClass); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } ... } }
即掃描我們設定的basepackage下的所有符合過濾器規則的介面(例如可以設定只掃描返回帶有mybatis @Mapper註解的介面),然後註冊成為spring bean,不過註冊的bean並不是MapperProxy,而是MapperFactoryBean,好吧,繼續往裡面看
MapperFactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } public SqlSession getSqlSession() { return new SqlSessionTemplate(sqlSessionFactory); } }
MapperFactoryBean這裡實現了FactoryBean介面,實際註冊的bean會通過getObject方法返回最終的實現類,終於到了我們的MapperProxy了
@MapperScan @MapperScans
這兩個mybatis-spring的註解其實就是用於自動幫我們註冊MapperScannerConfigurer 的spring Bean
SqlSessionFactoryBean
我們之前宣告SqlSessionFactory時要寫一堆程式碼,現在這個工作交給SqlSessionFactoryBean,其也繼承了spring FactoryBean介面,即通過getObject方法返回實際註冊的物件:SqlSessionFactory
7.mybatis-spring-boot-starter
mybatis-spring-boot-starter其實就是幫我們做一些自動化的配置,和spring-boot-starter的初衷一樣,這一塊其實沒有什麼好講的,所以我們就附屬到mybatis-spring的一個小章節裡
該專案pom裡引用了mybatis-spring-boot-autoconfigure,其spring.factories如下
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
MybatisLanguageDriverAutoConfiguration
就是幫我們自動設定LanguageDriver,例如FreeMarkerLanguageDriver、ThymeleafLanguageDriver等,mybatis預設是XMLLanguageDriver
MybatisAutoConfiguration
這裡主要自動幫我們註冊了SqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurer的Bean
主要還是MapperScannerConfigurer的Bean,就省去了我們之前還要手動去註冊MapperScannerConfigurer Bean,不過這裡有設定MapperScannerConfigurer 只掃描帶有mybatis @Mapper註解的介面。
到目前為止我們絕大多數場景只需要註冊一個SqlSessionFactoryBean為 spring bean就可以了
讀懂原始碼不難,講出來通俗易懂很難,寫出來通俗易懂是難上加難,文章寫出來不易,還望各位點點推薦,也歡迎評論區交流,你的互動也是我更新和維護的動力。