這裡的代理與設計模式中的代理模式密切相關,代理模式的主要作用是為其他物件提供一種控制對這個物件的存取方法,即在一個物件不適合或者不能直接參照另一個物件時,代理物件充當中介的作用。
現實生活中比較貼切的例子比如租房,被代理物件就是房東,代理物件就是中介,使用者就是租客,租客通過中介向房東租賃房屋,即使用者通過代理物件存取被代理物件。
一般我們通過new關鍵字初始化物件來呼叫類中的方法
如下程式碼,建立Human介面,Student類實現了Human介面,在main函數中,通過new關鍵字來初始化Student物件來實現對Student類中say()
方法的呼叫
interface Human{
public void say();
}
class Student implements Human{
@Override
public void say() {
System.out.println("I'm a Student");
}
}
public class ProxyTest {
public static void main(String[] args) {
Human human = new Student();
human.say();
}
}
//輸出
//I'm a Student
實現靜態代理有以下三個步驟:
建立介面,通過介面來實現物件的代理
建立該介面的實現類
建立Proxy代理類來呼叫我們需要的方法
interface Human{
public void say();
}
class Student implements Human{
@Override
public void say() {
System.out.println("I'm a Student");
}
}
class StudentProxy implements Human{
private Student student;
public StudentProxy(){}
public StudentProxy(Student student){
this.student = student;
}
private void begin(){
System.out.println("Begin");
}
private void end(){
System.out.println("End");
}
@Override
public void say() {
begin();
student.say();
end();
}
}
public class ProxyTest {
public static void main(String[] args) {
Student student = new Student();
StudentProxy studentProxy = new StudentProxy(student);
studentProxy.say();
}
}
//輸出
//Begin
//I'm a Student
//End
在上述程式碼中,我們在沒有修改Student類中say()
方法的情況下,實現了在原來的say()
方法前後分別執行sayHello()
和sayBye()
方法。由此引出代理模式的主要作用:
同時,靜態代理也存在一些比較致命的缺點。想象這樣一個場景:若新增一個Worker類實現了Human介面,我們應該如何去代理這個Worker類?比較容易想到的方法是擴大StudentProxy
的代理範圍,然後將Worker當作引數傳入StudentProxy
,然後繼續使用StudentProxy
類代理Worker物件。這樣實現功能是沒有問題的,但會存在如下問題:
由此引出動態代理
使用動態代理時,我們不需要編寫實現類,而是通過JDK提供的Proxy.newProxyInstance()
建立一個Human介面的物件。
生成動態代理有以下幾個步驟:
InvocationHandler
範例,它負責實現介面的方法呼叫;Proxy.newProxyInstance()
建立interface範例,它需要3個引數:
ClassLoader
,通常是介面類的ClassLoader
;InvocationHandler
範例。Object
強制轉型為介面。interface Human{
public void say();
}
class Student implements Human{
@Override
public void say() {
System.out.println("I'm a Student");
}
@Override
public void eat() {
System.out.println("I eat something");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object object;
public MyInvocationHandler(){}
public MyInvocationHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Begin");
Object invoke = method.invoke(object, args);
System.out.println("End");
return invoke;
}
}
public class ProxyTest {
public static void main(String[] args) {
MyInvocationHandler handler = new MyInvocationHandler(new Student());
Human human = (Human) Proxy.newProxyInstance(
Human.class.getClassLoader(),
new Class[] {Human.class},
handler);
human.say();
human.eat();
}
}
當Human介面的範例中方法增加時,如新增eat()方法時,只需要在Student類中直接範例化該方法即可。
當有其他不屬於Human類的子類需要被代理時,只需要將傳入MyInvocationHandler()
中的new Student()替換為需要被代理的子類即可。
綜上所述,通過動態代理基本可以解決靜態代理的痛點。
在Springboot專案中設定Mybatis時,我們僅編寫了Mapper介面,並未編寫Mapper介面的實現類,那麼當我們呼叫Mapper介面中方法時,是如何生成方法體的呢?
首先,專案在啟動時生成MapperFactoryBean物件,通過factory.getObject()
方法獲取mapper的代理物件
將上述過程與動態代理的步驟進行對比,我們最終獲取的是一個類似於動態代理例子中Human的代理物件,這裡是MapperProxy的代理物件。至此,一個Mapper代理物件就生成完畢。
然後,當我們完成專案中Mybatis的相關設定後,使用我們Mapper介面中的資料庫相關方法時,將呼叫之前生成的MapperProxy代理物件中invoke()
方法。類比動態代理的例子,即呼叫MyInvocationHandler類中的invoke()
方法。
//83行程式碼含義:如果method為Object中定義的方法(toString()、hash()...)則直接執行,這裡我們要執行的是Mapper介面中定義的方法,顯然返回為false
Object.class.equals(method.getDeclaringClass())
於是執行cachedInvoker(method)
的invoke()
方法
進入execute()
方法,我們看到之前我們設定的mapper.xml在MapperMethod初始化時,被解析成了59行的command。在此處通過sqlSession物件實現了對資料庫的操作。
至此,我們對Mybatis的資料庫操作流程已經有了大致瞭解。回到開頭的問題:為什麼僅編寫了Mapper介面,並未編寫Mapper介面的實現類,仍然可以實現我們的功能?這與我們之前的動態代理例子有什麼區別呢?
研究程式碼我們發現,我們並沒有直接使用method.invoke()
方法來呼叫實現類中的方法,而是呼叫了cachedInvoker(method)
的invoke()
方法解析我們設定的Mapper.xml,並通過sqlSession實現了資料庫操作,這個invoke()
方法相當於Mybatis自定義的方法。因此,這裡的invoke()
方法具體執行的邏輯是根據Mapper.xml設定來生成的,這個Mapper.xml設定可以理解為Mapper介面的實現類。