1. 程式人生 > >手撕MyBatis底層原始碼分析與實現

手撕MyBatis底層原始碼分析與實現

MyBatis Hiberante

簡介

什麼是 MyBatis ?

MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。

幫助改進文件…

不管你以何種方式發現了文件的不足,或是丟失對某一特性的描述,那麼你能做的最好的事情莫過於去研究它並把文件寫出來。 該文件 xdoc 格式的原始碼檔案可通過專案的 Git 程式碼庫來獲取。Fork 該原始碼庫,做出更新,然後提交一個 pull request 吧。 你將成為本文件的最佳作者,MyBatis 的使用者定會過來查閱的。

入門

安裝

要使用 MyBatis, 只需將 mybatis-x.x.x.jar 檔案置於 classpath 中即可。 如果使用 Maven 來構建專案,則需將下面的 dependency 程式碼置於 pom.xml 檔案中:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

從 XML 中構建 SqlSessionFactory

每個基於 MyBatis 的應用都是以一個 SqlSessionFactory 的例項為中心的。SqlSessionFactory 的例項可以通過 SqlSessionFactoryBuilder 獲得。而 SqlSessionFactoryBuilder 則可以從 XML 配置檔案或一個預先定製的 Configuration 的例項構建出 SqlSessionFactory 的例項。

從 XML 檔案中構建 SqlSessionFactory 的例項非常簡單,建議使用類路徑下的資原始檔進行配置。但是也可以使用任意的輸入流(InputStream)例項,包括字串形式的檔案路徑或者 file:// 的 URL 形式的檔案路徑來配置。MyBatis 包含一個名叫 Resources 的工具類,它包含一些實用方法,可使從 classpath 或其他位置載入資原始檔更加容易。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

XML 配置檔案(configuration XML)中包含了對 MyBatis 系統的核心設定,包含獲取資料庫連線例項的資料來源(DataSource)和決定事務作用域和控制方式的事務管理器(TransactionManager)。XML 配置檔案的詳細內容後面再探討,這裡先給出一個簡單的示例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

當然,還有很多可以在XML 檔案中進行配置,上面的示例指出的則是最關鍵的部分。要注意 XML 頭部的宣告,用來驗證 XML 文件正確性。environment 元素體中包含了事務管理和連線池的配置。mappers 元素則是包含一組 mapper 對映器(這些 mapper 的 XML 檔案包含了 SQL 程式碼和對映定義資訊)。

不使用 XML 構建 SqlSessionFactory

如果你更願意直接從 Java 程式而不是 XML 檔案中建立 configuration,或者建立你自己的 configuration 構建器,MyBatis 也提供了完整的配置類,提供所有和 XML 檔案相同功能的配置項。

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

注意該例中,configuration 添加了一個對映器類(mapper class)。對映器類是 Java 類,它們包含 SQL 對映語句的註解從而避免了 XML 檔案的依賴。不過,由於 Java 註解的一些限制加之某些 MyBatis 對映的複雜性,XML 對映對於大多數高階對映(比如:巢狀 Join 對映)來說仍然是必須的。有鑑於此,如果存在一個對等的 XML 配置檔案的話,MyBatis 會自動查詢並載入它(這種情況下, BlogMapper.xml 將會基於類路徑和 BlogMapper.class 的類名被載入進來)。具體細節稍後討論。

從 SqlSessionFactory 中獲取 SqlSession

既然有了 SqlSessionFactory ,顧名思義,我們就可以從中獲得 SqlSession 的例項了。SqlSession 完全包含了面向資料庫執行 SQL 命令所需的所有方法。你可以通過 SqlSession 例項來直接執行已對映的 SQL 語句。例如:

SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

誠然這種方式能夠正常工作,並且對於使用舊版本 MyBatis 的使用者來說也比較熟悉,不過現在有了一種更直白的方式。使用對於給定語句能夠合理描述引數和返回值的介面(比如說BlogMapper.class),你現在不但可以執行更清晰和型別安全的程式碼,而且還不用擔心易錯的字串字面值以及強制型別轉換。

例如:

SqlSession session = sqlSessionFactory.openSession();
try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();
}

現在我們來探究一下這裡到底是怎麼執行的。

探究已對映的 SQL 語句

現在,或許你很想知道 SqlSession 和 Mapper 到底執行了什麼操作,而 SQL 語句對映是個相當大的話題,可能會佔去文件的大部分篇幅。不過為了讓你能夠了解個大概,這裡會給出幾個例子。

在上面提到的兩個例子中,一個語句應該是通過 XML 定義,而另外一個則是通過註解定義。先看 XML 定義這個,事實上 MyBatis 提供的全部特性可以利用基於 XML 的對映語言來實現,這使得 MyBatis 在過去的數年間得以流行。如果你以前用過 MyBatis,這個概念應該會比較熟悉。不過 XML 對映檔案已經有了很多的改進,隨著文件的進行會愈發清晰。這裡給出一個基於 XML 對映語句的示例,它應該可以滿足上述示例中 SqlSession 的呼叫。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

對於這個簡單的例子來說似乎有點小題大做了,但實際上它是非常輕量級的。在一個 XML 對映檔案中,你想定義多少個對映語句都是可以的,這樣下來,XML 頭部和文件型別宣告佔去的部分就顯得微不足道了。檔案的剩餘部分具有很好的自解釋性。在名稱空間“org.mybatis.example.BlogMapper”中定義了一個名為“selectBlog”的對映語句,這樣它就允許你使用指定的完全限定名“org.mybatis.example.BlogMapper.selectBlog”來呼叫對映語句,就像上面的例子中做的那樣:

Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);

你可能注意到這和使用完全限定名呼叫 Java 物件的方法是相似的,之所以這樣做是有原因的。這個命名可以直接對映到在名稱空間中同名的 Mapper 類,並將已對映的 select 語句中的名字、引數和返回型別匹配成方法。這樣你就可以像上面那樣很容易地呼叫這個對應 Mapper 介面的方法。不過讓我們再看一遍下面的例子:

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

第二種方法有很多優勢,首先它不是基於字串常量的,就會更安全;其次,如果你的 IDE 有程式碼補全功能,那麼你可以在有了已對映 SQL 語句的基礎之上利用這個功能。

提示名稱空間的一點註釋

名稱空間(Namespaces)在之前版本的 MyBatis 中是可選的,這樣容易引起混淆因此毫無益處。現在名稱空間則是必須的,且意於簡單地用更長的完全限定名來隔離語句。

名稱空間使得你所見到的介面繫結成為可能,儘管你覺得這些東西未必用得上,你還是應該遵循這裡的規定以防哪天你改變了主意。出於長遠考慮,使用名稱空間,並將它置於合適的 Java 包名稱空間之下,你將擁有一份更加整潔的程式碼並提高了 MyBatis 的可用性。

命名解析:為了減少輸入量,MyBatis 對所有的命名配置元素(包括語句,結果對映,快取等)使用瞭如下的命名解析規則。

完全限定名(比如“com.mypackage.MyMapper.selectAllThings”)將被直接查詢並且找到即用。 短名稱(比如“selectAllThings”)如果全域性唯一也可以作為一個單獨的引用。如果不唯一,有兩個或兩個以上的相同名稱(比如“com.foo.selectAllThings ”和“com.bar.selectAllThings”),那麼使用時就會收到錯誤報告說短名稱是不唯一的,這種情況下就必須使用完全限定名。

對於像 BlogMapper 這樣的對映器類(Mapper class)來說,還有另一招來處理。它們的對映的語句可以不需要用 XML 來做,取而代之的是可以使用 Java 註解。比如,上面的 XML 示例可被替換如下:

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

對於簡單語句來說,註解使程式碼顯得更加簡潔,然而 Java 註解對於稍微複雜的語句就會力不從心並且會顯得更加混亂。因此,如果你需要做很複雜的事情,那麼最好使用 XML 來對映語句。

選擇何種方式以及對映語句的定義的一致性對你來說有多重要這些完全取決於你和你的團隊。換句話說,永遠不要拘泥於一種方式,你可以很輕鬆的在基於註解和 XML 的語句對映方式間自由移植和切換。

作用域(Scope)和生命週期

理解我們目前已經討論過的不同作用域和生命週期類是至關重要的,因為錯誤的使用會導致非常嚴重的併發問題。

提示 :物件生命週期和依賴注入框架

依賴注入框架可以建立執行緒安全的、基於事務的 SqlSession 和對映器(mapper)並將它們直接注入到你的 bean 中,因此可以直接忽略它們的生命週期。如果對如何通過依賴注入框架來使用 MyBatis 感興趣可以研究一下 MyBatis-Spring 或 MyBatis-Guice 兩個子專案。

SqlSessionFactoryBuilder

這個類可以被例項化、使用和丟棄,一旦建立了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 例項的最佳作用域是方法作用域(也就是區域性方法變數)。你可以重用 SqlSessionFactoryBuilder 來建立多個 SqlSessionFactory 例項,但是最好還是不要讓其一直存在以保證所有的 XML 解析資源開放給更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被建立就應該在應用的執行期間一直存在,沒有任何理由對它進行清除或重建。使用 SqlSessionFactory 的最佳實踐是在應用執行期間不要重複建立多次,多次重建 SqlSessionFactory 被視為一種程式碼“壞味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是應用作用域。有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。

SqlSession

每個執行緒都應該有它自己的 SqlSession 例項。SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。絕對不能將 SqlSession 例項的引用放在一個類的靜態域,甚至一個類的例項變數也不行。也絕不能將 SqlSession 例項的引用放在任何型別的管理作用域中,比如 Servlet 架構中的 HttpSession。如果你現在正在使用一種 Web 框架,要考慮 SqlSession 放在一個和 HTTP 請求物件相似的作用域中。換句話說,每次收到的 HTTP 請求,就可以開啟一個 SqlSession,返回一個響應,就關閉它。這個關閉操作是很重要的,你應該把這個關閉操作放到 finally 塊中以確保每次都能執行關閉。下面的示例就是一個確保 SqlSession 關閉的標準模式:

SqlSession session = sqlSessionFactory.openSession();
try {
  // do work
} finally {
  session.close();
}

在你的所有的程式碼中一致性地使用這種模式來保證所有資料庫資源都能被正確地關閉。

對映器例項(Mapper Instances)

對映器是一個你建立來繫結你對映的語句的介面。對映器介面的例項是從 SqlSession 中獲得的。因此從技術層面講,任何對映器例項的最大作用域是和請求它們的 SqlSession 相同的。儘管如此,對映器例項的最佳作用域是方法作用域。也就是說,對映器例項應該在呼叫它們的方法中被請求,用過之後即可廢棄。並不需要顯式地關閉對映器例項,儘管在整個請求作用域(request scope)保持對映器例項也不會有什麼問題,但是很快你會發現,像 SqlSession 一樣,在這個作用域上管理太多的資源的話會難於控制。所以要保持簡單,最好把對映器放在方法作用域(method scope)內。下面的示例就展示了這個實踐:

SqlSession session = sqlSessionFactory.openSession(); try { BlogMapper mapper = session.getMapper(BlogMapper.class); // do work } finally { session.close(); }

核心配置檔案mybatis.cfg.xml 在裡面配置 util MyBatisUtil.java package com.yxxy.mybatis.util;

import java.io.InputStream;

public class MyBatisUtil{ private static SqlSessionFactory sqlSessionFactory=null; private static SqlSession sqlSession=null; private static String resource=null;

public static SqlSessionFactory getSqlSessionFactory {
	resource="mybatis.cfg.xml";
	InputStream inputStream=null;
	try{
		inputStream=Resources.getResourceAsStream(resource);
		sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
    }catch(Exception e){
    	e.printStackTrace();
    }
    return sqlSessionFactory;
   }

mapper UserMapper.java

package com.yxxy.mybatis.mapper;

import com.yxxy.mybatis.entiey.User;

public interface UserMapper{

	User selectUserById(Integer id);
}

使用mybatis框架時根本不需要自己建立實現類,只有介面 在這裡插入圖片描述

test Test.java

package com.yxxy.mybatis.test;

import org.apache.ibatis.session.SqlSession;

public class Test{
	public static void main(String[] args){
		SqlSession sqlsession=MyBatisUtil.getSession();//打一個斷點,通過debug模式啟動
		UserMapper userMapper=sqlsession.getMapper(UserMapper.class);
		User user=userMapper.selectUserById(1);
		System.out.println(user);
		MyBatisUtil.closeSession();
	}
}

SqlSession sqlsession=MyBatisUtil.getSession();//打一個斷點,通過debug模式啟動

UserMapper userMapper=sqlsession.getMapper(UserMapper.class);//使用mybatis框架時根本不需要自己建立實現類,只有介面

首先要讀取配置檔案mybatis.cfg.xml和UserMapper.xml 一個載入了資料庫連線資訊,一個讀取我們編寫的SQL資訊 在這裡插入圖片描述

分析完原始碼後,自己寫一個這樣的框架 1、解析配置檔案–UserMapper.xml的解析工具類 硬編碼:直接寫死在那裡 2、實現動態代理–MapperProxy 3、載入代理物件並執行SQL–SqlSession 4、封裝JDBC–Executor介面和它的實現SimpleExecutor 5、mybatis的mapper介面–UserMapper類 6、對映資料庫實體–User類

UserMapper.xml namespace map key-value

MapperProxy 實現的invokahandler invoke方法method,args

SqlSession T getMapper T selectOne(String sql,)

Executor query

總共八個類實現 在這裡插入圖片描述

新建專案Java Project mybatis_hello ->將mysql的jar包匯入 ->新建包com.yxxy.mybatis.entity User.java UserMapperXml.java public class UserMapperXml{ //模擬UserMapper.xml的名稱空間 public static final String namespace=“com.yxxy.mybatis.mapper.UserMapper”; public static Map<String,String> map=new HashMap<>();

static{//模擬

->新建包com.yxxy.mybatis.executor Excutor.java public class Excutor{

SimpleExcutor.java 封裝JDBC實現CURD

public query(String sql,Object parameter){ Connection connection=null; PrepareStatement prepared

private Connection getConnection(){ String driver=""; String url=""; String username=""; String password=""; Class.forName(driver); Connection connection

->新建包com.yxxy.mybatis.mapper UserMapper.java public interface UserMapper{ User selectUserById(Integer id);

MapperProxy.java public class MapperProxy implements InvocationHandler{ private SqlSession sqlSession

public Objecy invoke(Object proxy,Method method,Ob){
if(){
String sql=UserMapperXml.map.get();

session SqlSession.java 載入代理物件執行SQL的類 public class SqlSession{ private Executor executor=new SimpleExecutor(); public T selectOne(String statement,Object patameter){ return executor.query(statement,parameter); } public T getMapper(Class clazz){ return Proxy.newIn