0. 前言
Mybatis是非常流行的ORM開發框架. 由最初的IBatis到今天的MyBatis, 功能越來越強大, 開發越來越便捷. 尤其是基於介面的開發, 使得開發者在編寫DAO時可以節省了很多時間. JAVA中介面不能被例項化, 因此不能直接呼叫介面. 那麼Mybatis是如何實現介面呼叫的呢?
本文將帶領大家瞭解Mybatis實現介面程式設計的關鍵 -- JAVA動態代理
1. Mybatis2.x
很早以前使用Mybatis2.x
版本編寫DAO時, 我們需要這麼寫:
public interface UserDAO {
Integer selectUserCount();
}
複製程式碼
public class IbatisUserDAO extends SqlMapClientDaoSupport implements UserDAO {
public Integer selectUserCount() {
return (Integer) this.getSqlMapClientTemplate().queryForObject("selectUserCount");
}
}
複製程式碼
<sqlMap namespace="User">
<select id="selectUserCount" resultClass="int">
select count(*) from t_user
</select>
</sqlMap>
複製程式碼
每個DAO介面都需要實現類並手動呼叫Mybatis的元件, Mybatis通過傳入的ID找到SQL並執行.
2. Mybatis3.x
從Mybatis3.x
開始支援基於介面程式設計, 開發者只需要定義DAO中的資料介面, 在Mapper
中編寫對應的SQL即可. 從此開發者不在需要手動呼叫Mybatis的元件, 這不僅節約了開發時間(無需提供DAO實現類), 更使得開發者不需要關心Mybatis的細節, 可以把更多精力放在業務資料介面上.
使用Mybatis3.x版本時編寫DAO時, 我們需要這麼寫:
public interface UserDAO {
Integer selectUserCount();
}
複製程式碼
<sqlMap namespace="User">
<select id="selectUserCount" resultClass="int">
select count(*) from t_user
</select>
</sqlMap>
複製程式碼
3. 動態代理
Mybatis中的DAO定義的是介面, 沒有實現類. 但是在JAVA中是無法直接呼叫介面, 那Mybatis是如何實現的基於介面程式設計的呢? 首先我們先熟悉一種設計模式:
3.1 代理模式
設計模式中有一種模式叫做代理模式: 假如有人欠你錢, 那麼你會找一個律師幫你打官司. 需要談判時會由你的律師幫你和對方進行談判. 那麼律師就相當於代理類, 你相當於委託類, 代理類全權代理你. 雙方談判時由代理類出面, 談好了需要簽字畫押時才需要你出手. 這就叫做代理模式.
為什麼需要代理類呢? 很簡單: 你不會談判, 律師比你更專業.
3.2 靜態代理
JAVA中有我們下面會講到的動態代理, 相比之下才有靜態代理一說, 靜態代理指的就是代理模式. 我們以上述的律師代理談判為例, 編寫一段程式碼:
你和律師都能談判和簽名, 因此定義一個含有談判和簽名方法的介面, 定義你和律師兩個類實現介面:
// 介面: 人
public interface Person {
// 談判
void talk();
// 簽名
void sign();
}
複製程式碼
// 你
public class You implements Person{
// 你的談判
@Override
public void talk() {
System.out.println("你開始談判");
}
// 你的簽名
@Override
public void sign() {
System.out.println("你開始簽名");
}
}
複製程式碼
// 律師
public class Lawer implements Person {
// 委託人(被代理人)
private Person client;
// 建立律師時必須指定委託人
public Lawer(Person client) {
this.client = client;
}
// 律師的談判
@Override
public void talk() {
// 談判不需要委託人出面, 由律師完成
// 因此不需要呼叫委託人的talk方法
System.out.println("律師開始談判");
}
// 律師的簽名
@Override
public void sign() {
// 律師不能代替委託人簽名
// 需要呼叫委託人的sign方法
client.sign();
}
}
複製程式碼
- 律師是代理別人去談判, 因此律師類中必須有委託人. 構造時傳入委託人.
- 律師談判時不需要委託人出面, 因此不需要呼叫委託人的talk方法.
- 簽名時必須由委託人簽名, 因此需要呼叫委託人的sign方法.
上述場景構建完成, 新建測試類:
public class Test {
public static void main(String[] args) {
// 需要談判
// 你找個律師代理你
Person lawer = new Lawer(new You());
// 開始談判
lawer.talk();
// 需要簽名
lawer.sign();
}
}
複製程式碼
當你需要談判時, 建立的是代理類(律師)並將委託類(你)傳入代理類中, 所有的操作都是對代理類(代理你的律師)進行操作. 總結一下代理模式的實現方式:
- 代理類與委託類實現同一介面.
- 代理類中需要指定委託類(即代理類代理了誰).
- 需要委託類時不直接返回委託類, 構建代理類返回.
使用代理模式有如下好處:
- 你不需要學習談判的技巧, 這些都是律師去做的, 你只需要做自己做的事.
- 你的朋友也需要談判時, 只需讓律師代理你的朋友即可, 他同樣不需要學習談判技巧.
- 你的很多朋友都委託律師時, 如果有新的談判技巧, 只需要律師一人學習就可以.
- 如果你覺得律師不好時, 只需要換一個律師. 你什麼都不需要改變.
- 當有很多人找你談判, 都會先找到律師, 由律師和你對接, 你是很安全的.
上述代理模式的優勢可以歸納為以下幾點:
- 許可權控制: 代理類負責控制委託類的訪問許可權. 即需要先和律師談判, 由律師決定找不找你.
- 業務解耦: 降低委託類的業務複雜度. 即你不需要學習很多知識, 比如談判技巧.
- 便於維護: 代理類實現通用業務, 委託類實現差異化. 通用業務變化時只修改代理類即可.
但如果你沒有使用代理模式, 你會發現你需要不停的學習各種談判技巧, 你的朋友需要談判時你需要教給他怎麼談判, 很多人直接和你談判時你會很累...
3.3 JAVA動態代理
代理模式雖然優勢很多, 但有這樣一個問題: 由於代理類與委託類都是實現同一介面, 當介面變化時, 所有代理類和委託類都需要相應的修改(隨介面增加或刪除方法). 比如上例介面增加了一個談判必備的喝酒功能, 你和律師都需要在各自類中增加相應方法.
Mybatis2.x
時很多專案都定義了DAO介面和實現類, 當業務頻繁變化時需要同步修改介面與實現類.
代理模式核心就是返回委託類的代理類, 如果能夠做到當呼叫委託類的方法時自動執行代理類的方法, 代理模式就會完美很多. JAVA通過反射機制提供了這種方式, 叫做動態代理.
動態代理可以實現: 當呼叫Person
介面中的任一方法時, 自動呼叫代理類中的方法.
3.3.1 建立動態代理類
動態代理類必須實現java.lang.reflect.InvocationHandler
介面的invoke
方法, 當呼叫Person
介面中任一方法時都將執行動態代理類的invoke
方法, 可以將代理類的業務邏輯寫在invoke
方法中.
// 動態代理類
// 需要實現InvocationHandler介面的invoke方法
public class PersonProxy implements InvocationHandler {
// 當呼叫被代理的介面的任何方法時都會執行到該方法
// proxy: 被呼叫的委託者物件(Person)
// method: 委託者被呼叫的方法物件(Person中被呼叫的方法)
// args: 委託者被呼叫的的方法的引數
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName());
return null;
}
}
複製程式碼
3.3.2 獲取動態代理類
呼叫java.lang.reflect.Proxy
類中的newProxyInstance
方法獲取動態代理類, 此時獲取到的代理類是Person
介面的代理類. 可以呼叫Person
中的方法.
public static void main(String[] args) {
// 動態代理類
PersonProxy proxy = new PersonProxy();
// 呼叫Proxy.newProxyInstance可獲取, newProxyInstance引數如下:
// 引數1: 生成代理類的類載入器
// 引數2: 代理的介面, JAVA根據介面在生成的代理類中新增介面的實現方法(執行引數3的invoke)
// 引數3: 實現InvocationHandler的動態代理類,當呼叫引數2介面中的方法時會執行該類的invoke方法
Person person = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
new Class[] { Person.class },
proxy);
// 呼叫介面的方法, 會自動執行到動態代理類的invoke中
person.talk();
person.sign();
}
複製程式碼
獲取到的代理類為呼叫newProxyInstance時傳入的第二個引數的代理類, 本例中傳入Person
介面, 獲取的到代理類為Person
的代理類, Person
為委託類.
3.3.3 動態代理原理
從程式的結構分析: 動態代理和靜態代理相比, 少定義了一個實現Person
介面的代理類. 實際上JAVA動態代理會自動生成代理模式所需的實現Person介面的代理類. 過程簡述如下:
- 呼叫
Proxy.newProxyInstance
獲取代理類時第二個引數傳入介面Person
. - JAVA會自動生成一個代理類並實現
Person
介面中的所有方法. - 每個方法的實現過程為通過反射呼叫第三個引數傳入的
PersonProxy
中的invoke
方法 - 將生成的代理類返回. 代理類實現了
Person
介面, 因此代理類中有Person
介面的所有方法.
JAVA動態代理自動生成的代理類, 示例如下:
// JAVA動態代理生成的代理類
public final class $Proxy0 extends Proxy implements Person {
// 實現Person的方法
@Override
public void talk() {
// 通過反射呼叫PersonProxy的invoke方法
// this.h為建立代理類時傳入的PersonProxy物件(第三個引數)
// m為PersonProxy的invoke方法
this.h.invoke(this, m, null);
}
// 實現Person的方法
@Override
public void sign() {
this.h.invoke(this, m, null);
}
}
複製程式碼
當JAVA動態代理生成完代理類後, 是不是和靜態代理的實現方式完全一樣了呢.
4. Mybatis實現原理
熟悉了JAVA動態代理, 我們來看一下Mybatis介面程式設計的實現原理.
- 獲取DAO(Spring注入)時, 實際獲取到的是DAO的代理類.
- 程式碼中呼叫DAO中的
selectUserCount
方法, 實際上呼叫的是DAO代理類的方法. - 代理類的
selectUserCount
方法中通過反射呼叫動態代理類(PersonProxy
)的invoke
方法. - 動態代理類(
PersonProxy
)的invoke
方法可以獲取DAO中被執行的方法名:selectUserCount
. - 通過
selectUserCount
在Mapper
檔案中找到對應的SQL並執行
5. 總結
JAVA動態代理的優勢在是Mybatis介面程式設計中體現的淋漓盡致. 本文介紹了代理模式並結合動態代理簡述了Mybatis介面程式設計的實現原理, Mybatis真正的實現要複雜的多. 當明白了其原理後, 當檢視Mybatis原始碼時可以少走很多彎路.
下一篇文章會結合專案實際應用案例介紹Mybatis攔截器的使用方式, 歡迎關注.