深入理解Java動態(tài)代理

代理模式

代理模式UML

使用代理模式創(chuàng)建代理對象,讓代理對象來控制對某個對象的訪問, 被代理對象可以是遠程對象,創(chuàng)建開銷大的對象或者需要安全控制的對象等.

Proxy 稱為代理對象.
RealSubject 是被代理的對象,也稱為委托對象.
Subject 是他們抽象出來的接口.

RealSubjectProxy都繼承自Subject, Proxy 內(nèi)部持有一個 RealSubject的變量,調(diào)用代理的方法,代理中將直接調(diào)用RealSubject對應的方法.

靜態(tài)代理

靜態(tài)代理,在編譯期間就需要指定好代理類,即在程序運行前就已經(jīng)存在代理類的字節(jié)碼文件烟逊,代理類和委托類的關系在運行前就確定了.

    interface Subject {
        void request();
    }

     class Proxy implements Subject {
        private RealSubject realSubject;

        public Proxy(RealSubject realSubject) {
            this.realSubject = realSubject;
        }

        @Override
        public void request() {
            // 添加log
            System.out.println("log...");
            realSubject.request();
        }
    }

     class RealSubject implements Subject {

        @Override
        public void request() {
            // todo ...
            System.out.println("request from http");
        }
    }

    public static void main(String[] args) {
        Subject proxy = new Proxy(new RealSubject());
        proxy.request();
    }

這樣做的優(yōu)點:

1. 可以隱藏委托類的實現(xiàn),可以進行權限控制和安全控制.

2. 實現(xiàn)客戶端和委托類解耦,只要對外接口不變,客戶端就不需要修改調(diào)用方式.

3. 通過擴展代理類伦吠,進行一些功能的附加與增強.

動態(tài)代理

動態(tài)代理類的字節(jié)碼在程序運行時由Java反射機制動態(tài)生成俺孙,無需程序員手工編寫它的源代碼.代理類和委托類的關系是在程序運行時確定。

相比于靜態(tài)代理, 動態(tài)代理的優(yōu)勢在于可以很方便的對代理類的函數(shù)進行統(tǒng)一的處理冯乘,而不用修改每個代理類的函數(shù)焚刚。

Java中的動態(tài)代理是使用Proxy.newProxyInstance()方法生成的.

先觀察下 Proxy.newProxyInstance()的參數(shù).

    Object newProxyInstance(
            ClassLoader loader,
            Class<?>[] interfaces,
            InvocationHandler h
    )

ClassLoader loader:

表示加載生成代理類的類加載器,通常情況下是直接使用
當前線程的類加載器 Thread.currentThread().getContextClassLoader(),
或者 使用 代理接口的類加載器. 如 Subject.class.getClassLoader().

少部分情況下,需要使用特殊的類加載器,如Android插件化中,使用動態(tài)代理,可能需要傳入插件對應的類加載器.

Class<?>[] interfaces:

這里,傳入你感興趣的 委托類所實現(xiàn)的接口.
如下面例子中的 Subject.class, 則你需要傳入 new Class[]{Subject.class}作為參數(shù).
如果 RealSubject實現(xiàn)了多個接口Subject1,Subject2,Subject3...,而你對其中Subject1,Subject2感興趣,
你可以傳入 new Class[]{Subject1.class,Subject2.class}, JVM運行時生成的字節(jié)碼類,將會實現(xiàn)傳入的這些接口.

InvocationHandler h

方法調(diào)度處理器接口. 內(nèi)部有一個回調(diào)方法 Object invoke(Object proxy, Method method, Object[] args).
實現(xiàn)該方法,可以攔截 上一個參數(shù)傳入的接口的方法, 你可以對這些方法進行增強, 甚至改變方法的行為.

實現(xiàn) InvocationHandler 通常需要傳入一個 委托對象, 然后在invoke()方法中,對委托對象進行修改或者增強操作.

來看一個簡單的動態(tài)代理的例子.

// proxy.Subject.java
public interface Subject {
    void doSomething();
}

// proxy.RealSubject.java
public class RealSubject implements Subject {
    @Override
    public void doSomething() {
        System.out.println("RealSubject do ...");
    }
}

// ReflectTest.java
public class ReflectTest {
    public static void main(String[] args) {
        // 委托對象
        RealSubject realSubject = new RealSubject();
        // 方法調(diào)度處理器
        InvocationHandler handler = new ProxyHandler(realSubject);

        // 生成代理對象
        Subject proxy = (Subject) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{Subject.class},
                handler
        );

        // 這里使用輔助類,保存JVM生成動態(tài)代理類到本地,此時先忽略
        ProxyUtils.generateClassFile(realSubject.getClass(), "SubjectProxy");

        // 代理運行方法
        proxy.doSomething();
    }

    // 代理方法調(diào)度器
    static class ProxyHandler implements InvocationHandler {
        // 委托對象
        RealSubject realSubject;

        ProxyHandler(RealSubject realSubject) {
            this.realSubject = realSubject;
        }

        /**
         * 在此方法中,進行委托對象方法的增強或者修改
         * 該示例中只是簡單的對其添加log.
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("do before ...");

            // 處理委托對象的方法
            Object result = method.invoke(realSubject, args);
            System.out.println("do after ...");
            return result;
        }
    }
}

//do before ...
//RealSubject do ...
//do after ...

從這里,我們可以窺探出 動態(tài)代理與靜態(tài)代理的一些差別,我們來總結一下.

動態(tài)代理使用 InvocationHandlerinvoke()方法來統(tǒng)一處理委托類的方法, 也就是說, 即使委托對象實現(xiàn)的接口中有幾百個方法,我們也只要在這一個方法中處理即可.換句話說,如果委托類增加了一些方法,而我們不需要對方法進行修改,那動態(tài)代理這部分的代碼,可以不需要改動,靜態(tài)代理則達不到這種效果.

動態(tài)代理在運行時,動態(tài)生成字節(jié)碼數(shù)據(jù),我們在編寫代碼的時候是看不到真正的代理類代碼.

動態(tài)代理一個重要的應用就是 AOP(面向切面編程), 主要處理 日志記錄濒蒋,性能統(tǒng)計,安全控制楼誓,事務處理玉锌,異常處理等等

如果我們需要對一些 不對外開發(fā)的或者不容易直接操作的類或者api進行操作,我們也可以使用 動態(tài)代理來處理. 如Android插件化中,對系統(tǒng)資源的HOOK, 如對ActivityManager的Hook就用到動態(tài)代理技術.

動態(tài)代理原理

Java的動態(tài)代理是通過 Proxy .newProxyInstance()方法來生成的. 我們來追蹤下源碼, 源碼中 我把無關和不打緊的代碼去掉,以便分析.

public class Proxy {
    // 生成的動態(tài)代理的構造參數(shù)類
    private static final Class<?>[] constructorParams = {InvocationHandler.class};

    // 動態(tài)代理類的緩存池
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
            proxyClassCache = new WeakCache<>(new KeyFactory(), new Proxy.ProxyClassFactory());

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
            throws IllegalArgumentException {
        final Class<?>[] intfs = interfaces.clone();
        // 1. 生成動態(tài)代理類
        Class<?> cl = getProxyClass0(loader, intfs);
        try {
            // 2. 反射創(chuàng)建動態(tài)代理類實例
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            return cons.newInstance(new Object[]{h});
        } catch (Exception e) {
            throw new InternalError(e.toString(), e);
        }
    }

    private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
        // 如果通過傳入的 類加載器和接口類已經(jīng)緩存過, 則直接從緩存中獲取 之前已經(jīng)生成的代理類
        // 否則, 將會通過ProxyClassFactory來創(chuàng)建一份動態(tài)代理類
        return proxyClassCache.get(loader, interfaces);
    }

    /**
     * 生成代理類的工廠
     */
    private static final class ProxyClassFactory
            implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
        // 生成的動態(tài)代理類的類名前綴
        private static final String proxyClassNamePrefix = "$Proxy";

        // 為生成的動態(tài)代理類加上數(shù)字標簽
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {

                // 1. 驗證傳入的接口類,是否是用傳入的 類加載器 加載的,不是則報錯
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(intf + " is not visible from class loader");
                }

                // 2. 驗證傳入的類是接口類型, 不是則報錯
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface");
                }

                // 3. 驗證是否傳入重復的接口類 , 是則報錯
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                            "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg    = null;     // package to define proxy class in
            int    accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            // 4. 對所有非公開的接口進行判斷,
            // 4.1 判斷所有的非公開接口,是不是在同一個包下, 不在 則報錯, 在則生成的代理類與其 同包名
            // 4.2 如果沒有非公開的接口,直接使用默認的包名 : com.sun.proxy
            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) {
                // if no non-public proxy interfaces, use com.sun.proxy package
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            // 5. 生成動態(tài)代理類的名稱
            // 形如 : com.sun.proxy.$Proxy0 com.sun.proxy.$Proxy1
            long   num       = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            // 6. 通過代理生成器 直接生成 代理類的 字節(jié)碼數(shù)據(jù) 即 .class類型的數(shù)據(jù)
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces, accessFlags);
            try {
                // 7. 將字節(jié)碼數(shù)據(jù)轉(zhuǎn)為 Class 類
                return defineClass0(loader, proxyName,
                        proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }
    }
}

從源碼中我們可以看出, 生成代理對象大步驟分為兩個.

  1. 生成動態(tài)代理類對象
  2. 反射生成動態(tài)代理實例對象

我們主要看第一個步驟. 它又分為好幾個小的流程, 在上面源碼分析中已經(jīng)很清楚的寫出來,這里就不在贅述.

它最終會調(diào)用 ProxyGenerator.generateProxyClass() 方法來生成字節(jié)碼文件.

然而這個過程 我們并不能拿到 這個生成的對象,也就是說 我們從代碼上是看不到這份文件的, 這對我們分析有很大的麻煩.

而既然 代碼是通過 ProxyGenerator.generateProxyClass()這個方法來生成, 我們可以嘗試通過這個方法來將生成的代碼保存到本地, 以便于分析.

public class ProxyUtils {
    /**
     * 根據(jù)類信息和提供的代理類名稱,生成字節(jié)碼并且保存到本地
     *
     * @param clazz     委托類
     * @param proxyName 生成代理類的名稱
     */
    public static void generateClassFile(Class<?> clazz, String proxyName) {
        // 根據(jù)類信息和提供的代理類名稱疟羹,生成字節(jié)碼
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String path      = clazz.getResource(".").getPath();
        System.out.println(path);
        FileOutputStream out = null;

        try {
            // 生成.class文件保存到本地
            out = new FileOutputStream(path + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

這個類可以幫助我們保存生成的代理類對象, 在上個例子中 有調(diào)用到過 ProxyUtils.generateClassFile(realSubject.getClass(), "SubjectProxy");,
我們直接打開生成的類來看下.

public final class SubjectProxy extends Proxy implements Subject {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public SubjectProxy(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 void doSomething() throws  {
        try {
            super.h.invoke(this, m3, (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("proxy.Subject").getMethod("doSomething");
            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());
        }
    }
}

可以看出, 生成的代理類, 是繼承 Proxy類,并且實現(xiàn)了Subject接口, 構造函數(shù)傳入 InvocationHandler對象.
然后,它將實現(xiàn)來的所有方法, 都通過反射的方式,調(diào)用 InvocationHandler.invoke()方法來實現(xiàn).
InvocationHandler中將傳入委托類 來完成反射調(diào)用.

也就是說, 代理類, 最終調(diào)用的是 通過InvocationHandler.invoke()方法進行增強或者修改的, 委托類(RealSubject)所對應的方法.

至此,我們分析完了, 動態(tài)代理實現(xiàn)的原理.

動態(tài)代理的不足

動態(tài)代理是一定要基于接口的, 如果委托對象沒有實現(xiàn)相應的接口, 是無法對其創(chuàng)建動態(tài)代理的.
JDK為我們提供的代理實現(xiàn)方案確實沒法解決這個問題, 那怎么辦呢? 可以使用 CGLib動態(tài)代理, 這里就不對其進行展開, 感興趣的可以自行搜索了解.

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末主守,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子榄融,更是在濱河造成了極大的恐慌参淫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愧杯,死亡現(xiàn)場離奇詭異涎才,居然都是意外死亡,警方通過查閱死者的電腦和手機力九,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門耍铜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人跌前,你說我怎么就攤上這事棕兼。” “怎么了抵乓?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵伴挚,是天一觀的道長。 經(jīng)常有香客問我灾炭,道長茎芋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任咆贬,我火速辦了婚禮败徊,結果婚禮上,老公的妹妹穿的比我還像新娘掏缎。我一直安慰自己皱蹦,他們只是感情好煤杀,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沪哺,像睡著了一般沈自。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辜妓,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天枯途,我揣著相機與錄音,去河邊找鬼籍滴。 笑死酪夷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的孽惰。 我是一名探鬼主播晚岭,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼勋功!你這毒婦竟也來了坦报?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤狂鞋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后字管,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡纤掸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年脐供,在試婚紗的時候發(fā)現(xiàn)自己被綠了浑塞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片政己。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖歇由,靈堂內(nèi)的尸體忽然破棺而出卵牍,到底是詐尸還是另有隱情,我是刑警寧澤沦泌,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布糊昙,位于F島的核電站谢谦,受9級特大地震影響萝衩,放射性物質(zhì)發(fā)生泄漏没咙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一牌捷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧暗甥,春花似錦捉捅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至合敦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間保檐,已是汗流浹背崔梗。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蒜魄,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓旅挤,卻偏偏與公主長得像伞鲫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子秕脓,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容