mybatis是怎樣煉成的
前言
一些個人感受:不管分析什麼源碼,如果我們能摸索出作者的心路歷程,跟着他的腳步一步一步往前走,這樣才能接近事實的真相,也能更平滑更有趣的學習到知識。跟福爾摩斯探案一樣,作者都經歷了些什麼,為什麼他要這樣去設計這樣去做,留給我們的只有無聲的代碼和那一段孤獨的日子。
閱讀順序建議是從上往下閱讀,如果直接跳轉到某一節,沒有基於上面的分析推理的話可能會不容易理解。
一切的一切要從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就可以了
讀懂源碼不難,講出來通俗易懂很難,寫出來通俗易懂是難上加難,文章寫出來不易,還望各位點點推薦,也歡迎評論區交流,你的互動也是我更新和維護的動力。