1.MyBatis應(yīng)用分析與實踐
2.MyBatis體系結(jié)構(gòu)與工作原理
3.MyBatis插件原理及Spring集成
4.手寫自己的MyBatis框架
本節(jié)目標(biāo):
1、 實現(xiàn) 1.0 版本,掌握 MyBatis 的本質(zhì)、核心功能、核心對象、執(zhí)行流程
2、 通過分析 2.0 版本,體驗框架的演進過程,理解 MyBatis
一,需求分析
假如你在一家軟件公司的研發(fā)部工作,有一天技術(shù)總監(jiān)老王想讓你負(fù)責(zé)開發(fā)一個項 目,你要做的第一件事情是什么?
確定需求。
那我們要開發(fā)這個項目,需求從哪里來? 我們要跟老王溝通下。
1、項目目標(biāo):為什么要做這個項目?做成什么樣?
老王說:我發(fā)現(xiàn)在業(yè)務(wù)復(fù)雜的項目中,開發(fā)的兄弟們用 JDBC 操作數(shù)據(jù)庫太麻煩了, 想要把一些基礎(chǔ)的操作做一個封裝和提取,讓開發(fā)的兄弟們更加專注于業(yè)務(wù)的開發(fā),這樣就可以提升開發(fā)效率,遠離 996。
原來是一個操作數(shù)據(jù)庫的框架。
那么我要問一下老王:這個項目要做什么,才簡化我們對數(shù)據(jù)庫的操作呢?或者說, 在業(yè)務(wù)復(fù)雜的項目中使用 JDBC
2、核心功能:這個框架需要解決什么問題?
老王給我看了一段 JDBC 的代碼:
-
它需要實現(xiàn)對連接資源的自動管理,也就是把創(chuàng)建 Connection、Statement、 關(guān)閉 Connection、Statement、ResultSet 這些操作封裝到底層的對象中,不需要在應(yīng)用層手動調(diào)用。
rs.close(); stmt.close(); conn.close(); -
它需要把 SQL 語句抽離出來實現(xiàn)集中管理,開發(fā)人員不用在業(yè)務(wù)代碼里面寫 SQL 語句。
String sql = "SELECT bid, name, author_id FROM blog where bid = 1"; ResultSet rs = stmt.executeQuery(sql); -
它需要實現(xiàn)對結(jié)果集的轉(zhuǎn)換,也就是我們指定了映射規(guī)則之后,這個框架會自動 幫我們把 ResultSet 映射成實體類對象。
Integer bid = rs.getInt("bid"); String name = rs.getString("name"); Integer authorId = rs.getInt("author_id"); blog.setAuthorId(authorId); blog.setBid(bid); blog.setName(name); 做了這些事以后,這個框架需要提供一個 API 來給我們操作數(shù)據(jù)庫,這里面封裝 了對數(shù)據(jù)庫的操作的常用的方法。
3、功能分解:這個框架要怎么解決這些問題?
老王的需求我已經(jīng)了解了,這個框架應(yīng)該怎么解決這些問題呢? 我們先來分析一下需要哪些核心對象:
-
核心對象
- 存放參數(shù)和結(jié)果映射關(guān)系、存放 SQL 語句,我們需要定義一個配置類;
- 執(zhí)行對數(shù)據(jù)庫的操作,處理參數(shù)和結(jié)果集的映射,創(chuàng)建和釋放資源,我們需要定 義一個執(zhí)行器;
- 有了這個執(zhí)行器以后,我們不能直接調(diào)用它,而是定義一個給應(yīng)用層使用的 API, 它可以根據(jù) SQL 的 id 找到 SQL 語句,交給執(zhí)行器執(zhí)行;
- 直接使用 id 查找 SQL 語句太麻煩了,我們干脆把存放 SQL 的命名空間定義成一 個接口,把 SQL 的 id 定義成方法,這樣只要調(diào)用接口方法就可以找到要執(zhí)行的 SQL。這 個時候我們需要引入一個代理類。
核心對象有了,接下來我們分析一下這個框架操作數(shù)據(jù)庫的主要流程,先從單條查詢?nèi)胧帧?/p>
操作流程(繪圖)

- 定義接口 Mapper 和方法,用來調(diào)用數(shù)據(jù)庫操作。 Mapper 接口操作數(shù)據(jù)庫需要通過代理類。
- 定義配置類對象 Configuration。
- 定義應(yīng)用層的 API SqlSession。它有一個 getMapper()方法,我們會從配置類 Configuration 里面使用 Proxy.newProxyInatance()拿到一個代理對象 MapperProxy。
- 有了代理對象 MapperProxy 之后,我們調(diào)用接口的任意方法,就是調(diào)用代理對 象的 invoke()方法。
- 代理對象 MapperProxy 的 invoke()方法調(diào)用了 SqlSession 的 selectOne()。
- SqlSession 只是一個 API,還不是真正的 SQL 執(zhí)行者,所以接下來會調(diào)用執(zhí)行器 Executor 的 query()方法。
- 執(zhí)行器 Executor 的 query()方法里面就是對 JDBC 底層的 Statement 的封裝, 最終實現(xiàn)對數(shù)據(jù)庫的操作,和結(jié)果的返回。
基于我們總結(jié)的這個框架的主要工作流程,接下來我們就要動手去寫這個框架了。 我們先給它起個名字叫 MyBatis-Custom
二,V1.0 的實現(xiàn)
創(chuàng)建一個全新的 maven 工程,命名為 mebatis,引入 mysql:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.javacoo</groupId>
<artifactId>MyBatis-Custom</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.test.skip>true</maven.test.skip>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<defaultGoal>package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
</plugin>
</plugins>
</build>
</project>
1、SqlSession
我們已經(jīng)分析了 MyBatis-Custom的主要對象和操作流程,應(yīng)該從哪里入手?
當(dāng)我們在 psvm 操作的時候,第一個需要的對象是 SqlSession。所以我們從應(yīng)用層的接口 SqlSession 入手。
那么我們先來創(chuàng)建一個 package,它是我們手寫的 MyBatis-Custom,我們建一個包。 首先我們創(chuàng)建一個自己的 SqlSession,叫 JCSqlSession。
根據(jù)我們剛才總結(jié)的流程圖,SqlSession 需要有一個獲取代理對象的方法,那么這 個代理對象是從哪里獲取到的呢?是從我們的配置類里面獲取到的,因為配置類里面有接口和它要產(chǎn)生的代理類的對應(yīng)關(guān)系。
所以,我們要先持有一個 Configuration 對象,叫 JCConfiguration,我們也創(chuàng)建這個類。除了獲取代理對象之外,Configuration 里面還存儲了我們的接口方法(也就是 statementId)和 SQL 語句的綁定關(guān)系。
第二個,我們在 SqlSession 中定義的操作數(shù)據(jù)庫的方法,最后都會調(diào)用 Executor 去操作數(shù)據(jù)庫,所以我們還要持有一個 Executor 對象,叫 JCExecutor,我們也創(chuàng)建它。
public class JCSqlSession {
private JCConfiguration configuration;
private JCExecutor executor;
...
}
除了這兩個屬性之外,我們還要定義 SqlSession 的行為,也就是它的主要的方法。
第一個方法是查詢方法,selectOne(),由于它可以返回任意類型,我們把返回值定 義成 T 泛型。selectOne()有兩個參數(shù),一個是 String 類型的 statementId,我們會 根據(jù)它找到 SQL 語句。一個是 Object 類型的 parameter 參數(shù)(可以是 Integer 也可以 是 String 等等,任意類型),用來填充 SQL 里面的占位符。
它會調(diào)用 Executor 的 query()方法,所以我們創(chuàng)建 Executor 類,傳入這兩個參數(shù), 一樣返回一個泛型。Executor 里面要傳入 SQL,但是我們還沒拿到,先用 statementId 代替。
public class JCSqlSession {
...
public <T> T selectOne(String statementId, Object paramater){
// 根據(jù)statementId拿到SQL
String sql = JCConfiguration.sqlMappings.getString(statementId);
if(null != sql && !"".equals(sql)){
return executor.query(sql, paramater );
}
return null;
}
}
JCExecutor.java
public class JCExecutor {
public <T> T query(String sql, Object paramater) {
return null;
}
}
第二個方法是獲取代理對象的方法,我們通過這種方式去避免了 statementId 的硬 編碼。
我們在 SqlSession 中創(chuàng)建一個 getMapper()的方法,由于可以返回任意類型的代理類,所以我們把返回值也定義成泛型 T。我們是根據(jù)接口類型獲取到代理對象的,所以傳入?yún)?shù)要用類型 Class。
public class JCSqlSession {
...
public <T> T getMapper(Class clazz){
return null;
}
}
2、Configuration
代理對象我們不是在 SqlSession 里面獲取到的,要進一步調(diào)用 Configuration 的 getMapper()方法。返回值需要強轉(zhuǎn)成(T)。
public class JCSqlSession {
...
public <T> T getMapper(Class clazz){
return (T)configuration.getMapper(clazz);
}
}
我們先在 Configuration 創(chuàng)建這個方法,返回類型一樣是泛型 T,先返回空。
public class JCConfiguration {
...
public <T> T getMapper(Class clazz) {
return null;
}
}
3、MapperProxy
我們要在 Configuration 中通過 getMapper()方法拿到這個代理對象,必須要有一 個實現(xiàn)了 InvocationHandler 的代理類。我們來創(chuàng)建它:JCMapperProxy。 提供一個 invoke()方法。
public class JCMapperProxy implements InvocationHandler {
...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
invoke()的實現(xiàn)我們先留著,先返回 null。MapperProxy 已經(jīng)有了,我們回到 Configuration.getMapper()完成獲取代理對象的邏輯。
返回代理對象,直接使用 JDK 的動態(tài)代理:第一個參數(shù)是類加載器,第二個參數(shù)是 被代理類,第三個參數(shù)是代理類。 把返回結(jié)果強轉(zhuǎn)為(T):
public class JCConfiguration {
...
public <T> T getMapper(Class clazz, JCSqlSession sqlSession) {
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{clazz},
new JCMapperProxy(sqlSession));
}
}
獲取代理類的邏輯已經(jīng)實現(xiàn)完了,我們可以在 SqlSession 中通過 getMapper()拿到代理對象了,也就是可以調(diào)用 invoke()方法了。
接下來去完成 MapperProxy 的 invoke() 方法。 在 MapperProxy 的 invoke()方法里面又調(diào)用了 SqlSession 的 selectOne()方法。 一個問題出現(xiàn)了:在 MapperProxy 里面根本沒有 SqlSession 對象?
這兩個對象的關(guān)系怎么建立起來?MapperProxy 怎么拿到一個 SqlSession 對象? 很簡單,我們可通過構(gòu)造函數(shù)傳入它。
先定義一個屬性,然后在 MapperProxy 的構(gòu)造函數(shù)里面賦值:
public class JCMapperProxy implements InvocationHandler {
private JCSqlSession sqlSession;
public JCMapperProxy(JCSqlSession sqlSession){
this.sqlSession = sqlSession;
}
...
}
因為修改了代理類的構(gòu)造函數(shù),這個時候 Configuration 創(chuàng)建代理類的方法 getMapper()也要修改。
問題:Configuration 的 getMapper()方法參數(shù)中也沒有 SqlSession,沒辦法傳給 MapperProxy 的構(gòu)造函數(shù)。怎么拿到 SqlSession 呢?是直接 new 一個嗎?
不需要,可以在 SqlSession 調(diào)用它的時候直接把自己傳進來:
public class JCConfiguration {
...
public <T> T getMapper(Class clazz, JCSqlSession sqlSession) {
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{clazz},
new JCMapperProxy(sqlSession));
}
}
那么 SqlSession 的 getMapper()方法也要修改:
public class JCSqlSession {
...
public <T> T getMapper(Class clazz){
return configuration.getMapper(clazz, this);
}
}
現(xiàn)在在 MapperProxy 里面已經(jīng)就可以拿到 SqlSession 對象了,在 invoke()方法里面我們會調(diào)用 SqlSession 的 selectOne()方法。
我們繼續(xù)來完成 invoke()方法。 selectOne()方法有兩個參數(shù), statementId 和 paramater,這兩個我們怎么拿到 呢?
statementId 其實就是接口的全路徑+方法名,中間加一個英文的點。
paramater 可以從方法參數(shù)中拿到,這里我們只傳了一個參數(shù),用 args[0]。
它要把 statementId 和參數(shù)傳給 SqlSession:
public class JCMapperProxy implements InvocationHandler {
private JCSqlSession sqlSession;
public JCMapperProxy(JCSqlSession sqlSession){
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String mapperInterface = method.getDeclaringClass().getName();
String methodName = method.getName();
String statementId = mapperInterface + "." + methodName;
return sqlSession.selectOne(statementId, args[0]);
}
}
4、Executor
到了 sqlSession 的 selectOne()方法,這里我們要去調(diào)用 Executor 的 query()方法, 這個時候我們必須傳入 SQL 語句和參數(shù)(根據(jù) statementId 獲?。?/p>
問題來了:我們怎么根據(jù) StatementId 找到我們要執(zhí)行的 SQL 語句呢?他們之間的 綁定關(guān)系我們配置在哪里?
為了簡便,免去讀取文件流和解析 XML 標(biāo)簽的麻煩,我們把我們的 SQL 語句放在 Properties 文件里面。
我們在 resources 目錄下創(chuàng)建一個 v1sql.properties 文件。key 就是接口全路徑+ 方法名稱,SQL 是我們的查詢 SQL。
參數(shù)這里,因為我們要傳入一個整數(shù),所以先用一個%d 的占位符代
com.javacoo.mybatis.v1.mapper.BlogMapper.selectBlogById=select * from blog where bid = %d
這個綁定關(guān)系是放在配置類 Configuration 里面的。
為了避免重復(fù)解析,我們在 Configuration 創(chuàng)建一個靜態(tài)屬性和靜態(tài)方法,直接解 析 v1sql.properties 文件里面的所有 KV 鍵值對:
public class JCConfiguration {
public static final ResourceBundle sqlMappings;
static{
sqlMappings = ResourceBundle.getBundle("v1sql");
}
public <T> T getMapper(Class clazz, JCSqlSession sqlSession) {
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{clazz},
new JCMapperProxy(sqlSession));
}
}
這樣就可以通過 Configuration 拿到 SQL 了。
如果 SQL 語句拿不到,說明不存在映射關(guān)系(或者不是接口中定義的操作數(shù)據(jù)的方 法,比如 toString()),我們返回空。
public class JCSqlSession {
...
public <T> T selectOne(String statementId, Object paramater){
// 根據(jù)statementId拿到SQL
String sql = JCConfiguration.sqlMappings.getString(statementId);
if(null != sql && !"".equals(sql)){
return executor.query(sql, paramater );
}
return null;
}
}
SQL 語句已經(jīng)拿到了,接下來就是 Executor 類的 query()方法,Executor 是數(shù)據(jù)庫 操作的真正執(zhí)行者。它里面應(yīng)該做什么事情?
我們干脆直接把 JDBC 的代碼全部復(fù)制過來,職責(zé)先不用細分。
參數(shù)用傳入的參數(shù)替換%d 占位符,需要 format 一下。
ResultSet rs = stmt.executeQuery(String.format(sql, paramater));
最后我們把結(jié)果強轉(zhuǎn)一下。
return (T)blog
寫一個測試類:
public class MyBatisTest {
public static void main(String[] args) {
JCSqlSession sqlSession = new JCSqlSession(new JCConfiguration(), new JCExecutor());
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
blogMapper.selectBlogById(1);
}
}
測試通過,1.0 的版本完成了:
Blog{bid=1, name='MyBatis 源碼分析', authorId='1001}
三,1.0 的不足
1.0 的功能完成了,在拿給老王看之前,我抽了根煙思考了一下:
V1.0 的不足
- 在 Executor 中,對參數(shù)、語句和結(jié)果集的處理是耦合的,沒有實現(xiàn)職責(zé)分離;
- 參數(shù):沒有實現(xiàn)對語句的預(yù)編譯,只有簡單的格式化(format),效率不高, 還存在 SQL 注入的風(fēng)險;
- 語句執(zhí)行:數(shù)據(jù)庫連接硬編碼;
- 結(jié)果集:還只能處理 Blog 類型,沒有實現(xiàn)根據(jù)實體類自動映射。 確實有點搓,拿不出手。
V1.0 的優(yōu)化目標(biāo)
- 支持參數(shù)預(yù)編譯;
- 支持結(jié)果集的自動處理(通過反射);
- 對 Executor 的職責(zé)進行細化。
V1.0 的功能增強目標(biāo)
- 在方法上使用注解配置 SQL;
- 查詢帶緩存功能;
- 支持自定義插件。
四、V2.0 的實現(xiàn)
1、配置文件
創(chuàng)建了全局配置文件 mybatis.properties,存放 SQL 連接信息、緩存開關(guān)、插件地 址、Mapper 接口地址。 全局配置文件在 Configuration 配置類的構(gòu)造器中解析。
2、參數(shù)處理
創(chuàng)建 ParameterHandler,調(diào)用 psmt 的 set 方法。propertie 文件中 SQL 語句的%d 占位符改成?。
3、結(jié)果集處理
創(chuàng)建 ResultSetHandler,在其中創(chuàng)建 pojo 對象,獲取 ResultSet 值,通過反射給 pojo 對象賦值。
實 體 類 的 轉(zhuǎn) 換 關(guān) 系 通 過 @Entity 注 解 ( 保 存 在 MapperRegistry 中 ) , 從 MapperProxyFactory(構(gòu)造函數(shù))——MapperProxy 一路傳遞到 ResultSetHandler 中。
4、語句執(zhí)行處理
創(chuàng)建 StatementHandler,在 Executor 中調(diào)用。封裝獲取連接的方法。
執(zhí)行查詢前調(diào)用 ParameterHandler,執(zhí)行查詢后調(diào)用 ResultSetHandler
5、支持注解配置 SQL
定義了一個@Select 注解,加在方法上。
在 Configuration 構(gòu) 造 函 數(shù) 中 的 parsingClass() 中解析, 保存在mappedStatements 中(一個 HashMap)。
注意:在 properties 中和注解上同時配置 SQL 語句,注解會覆蓋 properties。 properties 中對表達三個對象的映射關(guān)系并不適合,所以暫時用--分隔。注意類型 前面不能有空格。
6、支持查詢緩存
定 義 了 一 個 CachingExecutor , 當(dāng) 全 局 配 置 中 的 cacheEnabled=true 時 , Configuration 的 newExecutor()方法會對 SimpleExecutor 進行裝飾,返回被裝飾過的 Executor。CachingExecutor 中用 HashMap 維護緩存。 在 DefaultSqlSession 調(diào)用 Executor 時,會先走到裝飾器 CachingExecutor。 定義了一個 CacheKey 用于計算緩存 Key,主要根據(jù) SQL 語句和參數(shù)計算。
7、支持插件
定義了一個@Intercepts 注解,目前還只能攔截 Executor 的方法,所以屬性只要配置方法名稱。
定義 Interceptor 接口,是所有自定義插件必須實現(xiàn)的接口。
定義 InterceptorChain 容器,用來存放解析過的攔截器。在 Configuration 中創(chuàng)建 Executor 的時候,會調(diào)用它的 pluginAll()方法,對 Executor 循環(huán)代理。
定義 Invocation 包裝類,用于在執(zhí)行完自定義插件邏輯后調(diào)用 Executor 的原方法。
定義 Plugin 代理類,提供了一個 wrap()方法用于產(chǎn)生代理對象。當(dāng) Executor 被代 理后,所有的方法都會走到 invoke()方法中,進一步調(diào)用自定義插件的 intercept()方法。
完成了這些功能,我覺得應(yīng)該可以拿給老王看了。
五、V2.0 可優(yōu)化之處
老王看了2.0 的代碼以后,點了一根煙,提了一些建議:
1 、在 ResultSetHandler 中 , 類 型 處 理 都 是 寫 死 的 , 能 不 能 創(chuàng) 建 一 個 TypeHandler,把這些關(guān)系維護起來,處理所有類型的轉(zhuǎn)換關(guān)系和自定義類型; 2、只實現(xiàn)了@Select 的注解,插入、刪除、修改的注解呢?參數(shù)能不能用@Param 傳入類型?
3、插件只能攔截 Executor,能不能實現(xiàn)對其他核心對象的方法的攔截?插件可 以支持配置參數(shù)么?
4、緩存只有一級,不能在單個方法上關(guān)閉(properties 不夠用了),能不能實 現(xiàn)多級的緩存?
5、異常處理有點粗暴,都是直接 catch,沒有細化;
…… 小哥,接下來拯救世界的任務(wù)就交給你了……
工程源碼:https://gitee.com/javacoo/my-batis-custom
一些信息
路漫漫其修遠兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
郵箱:xihuady@126.com