mybatis是怎樣煉成的

RoyTian發表於2020-05-27

前言

一些個人感受:不管分析什麼原始碼,如果我們能摸索出作者的心路歷程,跟著他的腳步一步一步往前走,這樣才能接近事實的真相,也能更平滑更有趣的學習到知識。跟福爾摩斯探案一樣,作者都經歷了些什麼,為什麼他要這樣去設計這樣去做,留給我們的只有無聲的程式碼和那一段孤獨的日子。

閱讀順序建議是從上往下閱讀,如果直接跳轉到某一節,沒有基於上面的分析推理的話可能會不容易理解。

一切的一切要從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,還可以通過freemarkerthymeleaf 等格式來寫書寫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抽象出來的關鍵物件

我們要知道mybatismybatis-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就可以了

 

 

讀懂原始碼不難,講出來通俗易懂很難,寫出來通俗易懂是難上加難,文章寫出來不易,還望各位點點推薦,也歡迎評論區交流,你的互動也是我更新和維護的動力。

相關文章