JDK動態代理源碼分析

        上一篇中,分析了靜態代理和動態代理的區別,還留有一些問題,最核心的就是,爲什麼JDK動態代理必須要有一個接口,還有就是,定義一個InvocationHandler,並用Proxy.newProxyInstance調用,怎麼就可以實現動態代理效果。

        想要知道整個調用過程,就得深入源碼去看了。先說一下,本來想一篇文章寫完的,但是發現遇到了一個知識盲點:weakCache類以及它所採用的弱引用相關理論,而且1.8和1.6的實現邏輯也不一樣。就決定這篇先略過weakCache,晚點再寫。

        先看最Proxy.newProxyInstance方法,就不貼代碼了,裏面邏輯很簡單,畫了一個流程圖,標紅的是核心代碼。可以看到這個方法其實就是一個入口方法,用來封裝各種操作的。

               

        getProxyClass0這個方法也很簡單,調用proxyClassCache.get(loader, interfaces)方法進入到WeakCache類中,在這裏進行從緩存中取代理類,如果取不到則生成的功能。而proxyClassCache就是WeakCache的實例。

        簡單說一下,proxyClassCache類的初始化就包含了兩個工廠方法工廠方法,它內部使用了lambda表達式的BiFunction接口進行了調用。

 private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

       ProxyClassFactory類是Proxy的內部類,主要作用就是生成代理類的。裏面包含了兩個變量和一個方法,這個方法就是實現了BiFunction的apply,用於lambda調用。兩個變量是用來給代理類起名字用的,一個是標明類前綴:$Proxy,另一個是原子類AtomicLong,用於累加計數用。兩個合起來,就是我們在mybatis中經常看到的$Proxy145之類的日誌,這個就是調用的類名。

        主要來看apply方法,將大部分英文註釋翻譯成中文了:

 public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    //用IdentityHashMap存儲代理類,和hashMap的主要區別是他的鍵可以重複(值可以相同,但是對象地址不同)
    //因爲在JVM中,所有的對象都是獨立的,哪怕是同一個類產生的
    Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
    for (Class<?> intf : interfaces) {
        
        Class<?> interfaceClass = null;
        try {
            //通過接口名反射獲取接口類,但是不進行實例化。
            interfaceClass = Class.forName(intf.getName(), false, loader);
        } catch (ClassNotFoundException e) {
        }
        //判斷指定的類加載器(loader)加載接口所得到的Class對象(interfaceClass)是否與intf對象相同
        //沒懂這句話的目的。在什麼情況下,會出現不相等。
        if (interfaceClass != intf) {
            throw new IllegalArgumentException(
                intf + " is not visible from class loader");
        }
        //判斷該類是一個接口,所以這就是爲什麼jdk動態代理必須要有接口類的原因了
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException(
                interfaceClass.getName() + " is not an interface");
        }
        //判斷該接口是否重複
        if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
            throw new IllegalArgumentException(
                "repeated interface: " + interfaceClass.getName());
        }
    }

    String proxyPkg = null;     // 定義代理類將要生成的package路徑
    int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

    //判斷入參傳入的所有接口,是否都是public的,如果有一個非public類型的接口,那麼
    //判斷所有接口的包名是否一致,如果不一致,則拋錯。一致的話,包名就是這個非public接口的包名
    //也就是說要麼保證所有的接口都是public,要麼保證所有的接口在同一個包下面
    for (Class<?> intf : interfaces) {
        int flags = intf.getModifiers();
        if (!Modifier.isPublic(flags)) {
            accessFlags = Modifier.FINAL;
            String name = intf.getName();
            int n = name.lastIndexOf('.');
            String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
            if (proxyPkg == null) {
                proxyPkg = pkg;
            } else if (!pkg.equals(proxyPkg)) {
                throw new IllegalArgumentException(
                    "non-public interfaces from different packages");
            }
        }
    }
    if (proxyPkg == null) {
        // 如果所有接口都是public的,那麼就使用 com.sun.proxy package
        proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
    }
    //根據上面提到的兩個變量,生成一個代理類名稱
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
    //創建代理類
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
        proxyName, interfaces, accessFlags);
    try {
        return defineClass0(loader, proxyName,
                            proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
    }
}

        然後就是調用ProxyGenerator.generateProxyClass方法來生成代理類的class文件。這個類在sun.misc下,據說從1.8起oracle不再放出這個包下的代碼。因爲我是使用eclipse中的反編譯打開的,裏面有一堆??,看着有點費事,而且我覺得這個太底層了,也沒必要研究這麼細,所以源碼就不貼了,簡單說一下個人理解吧。

        就是將生成每個類固有的hashcode,equals,toString等方法,加上接口中定義的方法,以及構造方法等一個類所應該給具有的信息按照字節碼的規範生成在內存中。

        等返回這個類的對象後,會調用defineClass0這個本地方法(native)去生成對應的實例,最終返回給代理類對象進行調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章