爲何JDK的動態代理要基於接口實現而不能基於繼承實現?

掃描文末二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析文章java

1. 問題

在閱讀本文以前,能夠先思考一下下面幾個問題。數組

  • 爲何說JDK的動態代理要基於接口實現,而不能基於繼承來實現?
  • 在JDK的動態代理中,在目標對象方法內部調用本身的另外一個方法時,另外一個方法在執行時,爲何沒有通過代理對象?

2. JDK的動態代理的固定寫法

  • JDK的動態代理的寫法比較固定,須要先定義一個接口和接口的實現類,而後再定義一個實現了InvocationHandler接口的實現類。而後調用Proxy類的newInstance()方法便可。示例代碼以下:
  • 先定義一個接口:UserService,接口中有兩個方法
public interface UserService {

    int insert();

    String query();
}
複製代碼
  • 再定義一個UserService接口的實現類:UserServiceImpl
public class UserServiceImpl implements UserService{

    @Override
    public int insert() {
        System.out.println("insert");
        return 0;
    }

    @Override
    public String query() {
        System.out.println("query");
        return null;
    }
}
複製代碼
  • 再定義一個InvocationHandler接口的實現類:UserServiceInvocationHandler。在自定義的InvocationHandler中,定義了一個屬性:target,定義這個屬性的目的是爲了在InvocationHandler中持有對目標對象的引用,target屬性的初始化是在構造器中進行初始化的。
public class UserServiceInvocationHandler implements InvocationHandler {

    // 持有目標對象
    private Object target;

    public UserServiceInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invocation handler");
        // 經過反射調用目標對象的方法
        return method.invoke(target,args);
    }
}
複製代碼
  • 經過Proxy.newProxyInstance()方法建立代理對象
public class MainApplication {

    public static void main(String[] args) {
        // 指明一個類加載器,要操做class文件,怎麼少得了類加載器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 爲代理對象指定要是實現哪些接口,這裏咱們要爲UserServiceImpl這個目標對象建立動態代理,因此須要爲代理對象指定實現UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一個InvocationHandler,並初始化InvocationHandler中的目標對象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立動態代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 執行代理對象的方法,經過觀察控制檯的結果,判斷咱們是否對目標對象(UserServiceImpl)的方法進行了加強
        userService.insert();
    }
}
複製代碼
  • 控制檯打印結果以下,從打印結果來看,已經完成了對目標對象UserServiceImpl的代理(加強)。

控制檯結果1

3. 如何查看代理類的class文件

閱讀到這裏,咱們基本知道了JDK動態代理的寫法,功能雖然實現了,可是對於喜歡研究源碼的同窗,可能會思考:爲何調用Proxy.newProxyInstance()方法後,就產生了代理對象,對目標對象的方法進行了加強?原理是什麼呢?微信

  • 一般學習某項技術的原理,都是看源文件.java或者藉助反編譯工具反編譯.class文件後去分析。但是對於JDK的動態代理,它產生的代理對象是在運行時建立的,經過常規操做,咱們沒辦法獲得這個代理對象對應的.class文件。若是能獲得代理對象多對應的class文件,那動態代理的原理,咱們就好分析了。
  • 因此接下來介紹兩種獲取到代理對象的.class文件的方法

3.1 手動寫到磁盤

  • 在調用Proxy.newProxyInstance()方法時,最終會調用到ProxyGenerator.generateProxyClass()方法,該方法的做用就是生成代理對象的class文件,返回值是一個byte[]數組。因此咱們能夠將生成的byte[]數組經過輸出流,將內容寫出到磁盤。
public static void main(String[] args) throws IOException {
    String proxyName = "com.tiantang.study.$Proxy0";
    Class[] interfaces = new Class[]{UserService.class};
    int accessFlags = Modifier.PUBLIC;
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
    // 將字節數組寫出到磁盤
    File file = new File("/Users/liujinkun/Downloads/dynamic/$Proxy0.class");
    OutputStream outputStream = new FileOutputStream(file);
    outputStream.write(proxyClassFile);
}
複製代碼
  • 運行完main()方法後,從目錄中/Users/liujinkun/Downloads/dynamic/$Proxy0.class找到生成的文件,因爲是一個class文件,因此咱們須要把它有反編譯器編譯一下,例如:在idea中,將文件放到target目錄下,打開文件就能看到反編譯後的代碼了。

3.2 自動寫到磁盤

  • 第二種方法是經過百度查來的,在代碼中添加一行代碼:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"),就能實現將程序運行過程當中產生的動態代理對象的class文件寫入到磁盤。以下示例:
public class MainApplication {

    public static void main(String[] args) {
        // 讓代理對象的class文件寫入到磁盤
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 指明一個類加載器,要操做class文件,怎麼少得了類加載器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 爲代理對象指定要是實現哪些接口,這裏咱們要爲UserServiceImpl這個目標對象建立動態代理,因此須要爲代理對象指定實現UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一個InvocationHandler,並初始化InvocationHandler中的目標對象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立動態代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 執行代理對象的方法,經過觀察控制檯的結果,判斷咱們是否對目標對象(UserServiceImpl)的方法進行了加強
        userService.insert();
    }
}
複製代碼
  • 運行程序,最終發如今項目的根目錄下出現了一個包:com.sun.proxy。包下有一個文件$Proxy0.class。在idea打開,發現就是所產生代理類的源代碼。

4. 問題解答

經過上面兩種方法獲取到代理對象的源代碼以下:異步

package com.sun.proxy;

import com.tiantang.study.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UserService {
    private static Method m1;
    private static Method m3;
    private static Method m4;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int insert() throws {
        try {
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String query() throws {
        try {
            return (String)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.tiantang.study.UserService").getMethod("insert");
            m4 = Class.forName("com.tiantang.study.UserService").getMethod("query");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
複製代碼
  • 經過源碼咱們發現,$Proxy0類繼承了Proxy類,同時實現了UserService接口。到這裏,咱們的問題一就能解釋了,爲何JDK的動態代理只能基於接口實現,不能基於繼承來實現?由於Java中不支持多繼承,而JDK的動態代理在建立代理對象時,默認讓代理對象繼承了Proxy類,因此JDK只能經過接口去實現動態代理。ide

  • $Proxy0實現了UserService接口,因此重寫了接口中的兩個方法($Proxy0同時還重寫了Object類中的幾個方法)。因此當咱們調用query()方法時,先是調用到$Proxy0.query()方法,在這個方法中,直接調用了super.h.invoke()方法,父類是Proxy,父類中的h就是咱們定義的InvocationHandler,因此這兒會調用到UserServiceInvocationHandler.invoke()方法。所以當咱們經過代理對象去執行目標對象的方法時,會先通過InvocationHandler的invoke()方法,而後在經過反射method.invoke()去調用目標對象的方法,所以每次都會先打印invocation handler這句話。工具

public class UserServiceInvocationHandler implements InvocationHandler {

    // 持有目標對象
    private Object target;

    public UserServiceInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invocation handler");
        // 經過反射調用目標對象的方法
        return method.invoke(target,args);
    }
}
複製代碼
  • 對於文章開頭的第二個問題,咱們先來看下以下示例代碼
public class UserServiceImpl implements UserService{

    @Override
    public int insert() {
        System.out.println("insert");
        query();
        return 0;
    }

    @Override
    public String query() {
        System.out.println("query");
        return null;
    }
}
複製代碼
public class MainApplication {

    public static void main(String[] args) {
        // 讓代理對象的class文件寫入到磁盤
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 指明一個類加載器,要操做class文件,怎麼少得了類加載器呢
        ClassLoader classLoader = MainApplication.class.getClassLoader();
        // 爲代理對象指定要是實現哪些接口,這裏咱們要爲UserServiceImpl這個目標對象建立動態代理,因此須要爲代理對象指定實現UserService接口
        Class[] classes = new Class[]{UserService.class};
        // 初始化一個InvocationHandler,並初始化InvocationHandler中的目標對象
        InvocationHandler invocationHandler = new UserServiceInvocationHandler(new UserServiceImpl());
        // 建立動態代理
        UserService userService = (UserService) Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        // 執行代理對象的方法,經過觀察控制檯的結果,判斷咱們是否對目標對象(UserServiceImpl)的方法進行了加強
        userService.insert();
    }
}
複製代碼
  • 在這段代碼中,只修改了UserServiceImpl的insert()方法,在打印以後,咱們調用了一下query()方法。在看打印結果以前,咱們先來分析下打印結果:按照咱們的理解$Proxy0代理類對UserServiceImpl中的方法都進行了加強,每次調用UserServiceImpl中類的方法時,應該都會通過InvocationHandler中的invoke()方法,每次都會打印invocation handler這句話。因此當咱們調用userService.insert()時,打印結果應該是:
invocation handler
insert
invocation handler
query
複製代碼
  • 而後,結果然的如咱們想象的那樣嗎?下面截圖爲程序運行的結果:

控制檯結果2

  • 從結果中,咱們發現,只打印了一次invocation handler。在調用query()方法時,並無去執行InvocationHandler中的invoke()方法。爲何呢?緣由就在這個地方:
public int insert() {
    System.out.println("insert");
    query();
    return 0;
}
複製代碼
  • 在insert()方法中調用query()方法時,實際上調用的是this.query(),而此時this對象是UserServiceImpl,並非$Proxy0這個代理對象,只有在調用代理對象的query()方法時,纔會通過InvocationHandler.invoke()方法,因此此時只會打印一次invocation handler。因此第二個問題的答案就是:在調用過程當中,this的指向發生了變化。

5. 總結與思考

  • 本文介紹了JDK動態代理的常規用法,而後經過特殊手段,獲取到了代理對象的class文件,而後根據class文件,分析了JDK動態代理中兩個比較常見的問題。
  • 看到這裏相信你已經明白了JDK動態代理的原理,以及JDK動態代理所帶來的問題——在目標對象的方法A內部調用目標對象的其餘方法B,此時動態代理並不會對方法B進行加強。
  • 相信不少人曾經碰到過在Spring項目中,經過@Transactional來聲明事務時,出現了事務不生效的狀況,結合本文,如今相信你應該能明白這其中的問題了吧?(關於Spring事務失效的例子,能夠百度搜索: JDK 動態代理 事務失效)
  • 一樣的案例還有@Async等案例,在同一個類的兩個方法中開啓異步,而後在方法內部進行互相調用,最終致使異步失效的問題。
  • 既然JDK動態代理存在這些問題,那如今Spring中是如何避免這些問題的呢?固然是大名鼎鼎的CGLIB了,關於CGLIB,下一篇文章分析。

6. 猜你喜歡

掃描下方二維碼便可關注微信公衆號菜鳥飛呀飛,一塊兒閱讀更多Spring源碼。源碼分析

微信公衆號
相關文章
相關標籤/搜索