Java 靜態代理、Java動態代理、CGLIB動態代理

開篇

Java 的代理就是客戶類不再直接和委托類打交道, 而是通過一個中間層來訪問, 這個中間層就是代理。為啥要這樣呢, 是因為使用代理有 2 個優勢:

可以隱藏委托類的實現

可以實現客戶與委托類之間的解耦, 在不修改委托類代碼的情況下能夠做一些額外的處理

我們舉個很常見的例子: 工廠會生產很多的玩具, 但是我們買玩具都是到商店買的, 而不是到工廠去買的, 工廠怎么生產我們并不關心, 我們只知道到商店可以買到自己想要的玩具,并且,如果我們需要送人的話商店可以把這些玩具使用禮品盒包裝。這個工廠就是委托類, 商店就是代理類, 我們就是客戶類。

在 Java 中我們有很多場景需要使用代理類, 比如遠程 RPC 調用的時候我們就是通過代理類去實現的, 還有 Spring 的 AOP 切面中我們也是為切面生成了一個代理類等等。
代理類主要分為靜態代理、JDK 動態代理和 CGLIB 動態代理,它們各有優缺點,沒有最好的, 存在就是有意義的,在不同的場景下它們會有不同的用武之地。

1. Java 靜態代理
首先, 定義接口和接口的實現類, 然后定義接口的代理對象, 將接口的實例注入到代理對象中, 然后通過代理對象去調用真正的實現類,實現過程非常簡單也比較容易理解, 靜態代理的代理關系在編譯期間就已經確定了的。它適合于代理類較少且確定的情況。它可實現在怒修改委托類代碼的情況下做一些額外的處理,比如包裝禮盒,實現客戶類與委托類的解耦。缺點是只適用委托方法少的情況下, 試想一下如果委托類有幾百上千個方法, 豈不是很難受, 要在代理類中寫一堆的代理方法。這個需求動態代理可以搞定

// 委托接口
public interface IHelloService {

    /**
     * 定義接口方法
     * @param userName
     * @return
     */
    String sayHello(String userName);

}
// 委托類實現
public class HelloService implements IHelloService {

    @Override
    public String sayHello(String userName) {
        System.out.println("helloService" + userName);
        return "HelloService" + userName;
    }
}

// 代理類
public class StaticProxyHello implements IHelloService {

    private IHelloService helloService = new HelloService();

    @Override
    public String sayHello(String userName) {
        /** 代理對象可以在此處包裝一下*/
        System.out.println("代理對象包裝禮盒...");
        return helloService.sayHello(userName);
    }
}
// 測試靜態代理類
public class MainStatic {
    public static void main(String[] args) {
        StaticProxyHello staticProxyHello = new StaticProxyHello();
        staticProxyHello.sayHello("isole");
    }
}

2. 動態代理技術
代理類在程序運行時創建的代理方式被成為 動態代理。在了解動態代理之前, 我們先簡回顧一下 JVM 的類加載機制中的加載階段要做的三件事情
( 附 Java 中的類加載器 )

通過一個類的全名或其它途徑來獲取這個類的二進制字節流

將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構

在內存中生成一個代表這個類的 Class 對象, 作為方法區中對這個類訪問的入口

而我們要說的動態代理,主要就發生在第一個階段, 這個階段類的二進制字節流的來源可以有很多, 比如 zip 包、網絡、運行時計算生成、其它文件生成 (JSP)、數據庫獲取。其中運行時計算生成就是我們所說的動態代理技術,在 Proxy 類中, 就是運用了 ProxyGenerator.generateProxyClass 來為特定接口生成形式為 *$Proxy 的代理類的二進制字節流。所謂的動態代理就是想辦法根據接口或者目標對象計算出代理類的字節碼然后加載進 JVM 中。實際計算的情況會很復雜,我們借助一些諸如 JDK 動態代理實現、CGLIB 第三方庫來完成的

另一方面為了讓生成的代理類與目標對象 (就是委托類) 保持一致, 我們有 2 種做法:通過接口的 JDK 動態代理 和通過繼承類的 CGLIB 動態代理。(還有一個使用了 ASM 框架的 javassist 太復雜了,我還沒研究過, 這里TODO下)

3. JDK 動態代理
在 Java 的動態代理中, 主要涉及 2 個類,java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler
我們需要一個實現 InvocationHandler 接口的中間類, 這個接口只有一個方法 invoke 方法, 方法的每個參數的注釋如下代碼。

我們對處理類中的所有方法的調用都會變成對 invoke 方法的調用,這樣我們可以在 invoke 方法中添加統一的處理邏輯(也可以根據 method 參數判斷是哪個方法)。中間類 (實現了 InvocationHandler 的類) 有一個委托類對象引用, 在 Invoke 方法中調用了委托類對象的相應方法,通過這種聚合的方式持有委托類對象引用,把外部對 invoke 的調用最終都轉為對委托類對象的調用。

實際上,中間類與委托類構成了靜態代理關系,在這個關系中,中間類是代理類,委托類是委托類。然后代理類與中間類也構成一個靜態代理關系,在這個關系中,中間類是委托類,代理類是代理類。也就是說,動態代理關系由兩組靜態代理關系組成,這就是動態代理的原理。

public interface InvocationHandler {
    /**
     * 調用處理
     * @param proxy 代理類對象
     * @param methon 標識具體調用的是代理類的哪個方法
     * @param args 代理類方法的參數
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
Demo 如下:

// 委托類接口
public interface IHelloService {

    /**
     * 方法1
     * @param userName
     * @return
     */
    String sayHello(String userName);

    /**
     * 方法2
     * @param userName
     * @return
     */
    String sayByeBye(String userName);

}
// 委托類
public class HelloService implements IHelloService {

    @Override
    public String sayHello(String userName) {
        System.out.println(userName + " hello");
        return userName + " hello";
    }

    @Override
    public String sayByeBye(String userName) {
        System.out.println(userName + " ByeBye");
        return userName + " ByeBye";
    }
}
// 中間類
public class JavaProxyInvocationHandler implements InvocationHandler {

    /**
     * 中間類持有委托類對象的引用,這里會構成一種靜態代理關系
     */
    private Object obj ;

    /**
     * 有參構造器,傳入委托類的對象
     * @param obj 委托類的對象
     */
    public JavaProxyInvocationHandler(Object obj){
        this.obj = obj;

    }

    /**
     * 動態生成代理類對象,Proxy.newProxyInstance
     * @return 返回代理類的實例
     */
    public Object newProxyInstance() {
        return Proxy.newProxyInstance(
                //指定代理對象的類加載器
                obj.getClass().getClassLoader(),
                //代理對象需要實現的接口,可以同時指定多個接口
                obj.getClass().getInterfaces(),
                //方法調用的實際處理者,代理對象的方法調用都會轉發到這里
                this);
    }


    /**
     *
     * @param proxy 代理對象
     * @param method 代理方法
     * @param args 方法的參數
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke before");
        Object result = method.invoke(obj, args);
        System.out.println("invoke after");
        return result;
    }
}
// 測試動態代理類
public class MainJavaProxy {
    public static void main(String[] args) {
        JavaProxyInvocationHandler proxyInvocationHandler = new JavaProxyInvocationHandler(new HelloService());
        IHelloService helloService = (IHelloService) proxyInvocationHandler.newProxyInstance();
        helloService.sayByeBye("paopao");
        helloService.sayHello("yupao");
    }

}

在上面的測試動態代理類中, 我們調用 Proxy 類的 newProxyInstance 方法來獲取一個代理類實例。這個代理類實現了我們指定的接口并且會把方法調用分發到指定的調用處理器。

首先通過 newProxyInstance 方法獲取代理類的實例, 之后就可以通過這個代理類的實例調用代理類的方法,對代理類的方法調用都會調用中間類 (實現了 invocationHandle 的類) 的 invoke 方法,在 invoke 方法中我們調用委托類的對應方法,然后加上自己的處理邏輯。

java 動態代理最大的特點就是動態生成的代理類和委托類實現同一個接口。java 動態代理其實內部是通過反射機制實現的,也就是已知的一個對象,在運行的時候動態調用它的方法,并且調用的時候還可以加一些自己的邏輯在里面。(附: Java 反射)

3.2 Proxy.newProxyInstance 源碼閱讀

上面說過, Proxy.newProxyInstance 通過反射機制用來動態生成代理類對象, 為接口創建一個代理類,這個代理類實現這個接口。具體源碼如下:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        // 檢查空指針
        Objects.requireNonNull(h);
        // 用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象
        final Class<?>[] intfs = interfaces.clone();
        // 獲取系統的安全接口,不為空的話需要驗證是否允許訪問這種關系的代理訪問
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 生成代理類 Class,通過類加載器和接口
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 通過構造器來創建實例
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            //獲取所有的構造器
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            // 構造器不是public的話需要設置可以訪問
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            // 返回創建的代理類Class的實例對象
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

4. CGLIB 動態代理
JDK 動態代理依賴接口實現,而當我們只有類沒有接口的時候就需要使用另一種動態代理技術 CGLIB 動態代理。首先 CGLIB 動態代理是第三方框架實現的,在 maven 工程中我們需要引入 cglib 的包, 如下:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2</version>
</dependency>

CGLIB 代理是針對類來實現代理的,原理是對指定的委托類生成一個子類并重寫其中業務方法來實現代理。代理類對象是由 Enhancer 類創建的。CGLIB 創建動態代理類的模式是:

查找目標類上的所有非 final 的 public 類型的方法 (final 的不能被重寫)

將這些方法的定義轉成字節碼

將組成的字節碼轉換成相應的代理的 Class 對象然后通過反射獲得代理類的實例對象

實現 MethodInterceptor 接口, 用來處理對代理類上所有方法的請求


// 委托類,是一個簡單類
public class CglibHelloClass {
   /**
    * 方法1
    * @param userName
    * @return
    */
   public String sayHello(String userName){
       System.out.println("目標對象的方法執行了");
       return userName + " sayHello";
   }

   public String sayByeBye(String userName){
       System.out.println("目標對象的方法執行了");
       return userName + " sayByeBye";
   }

}
/**
* CglibInterceptor 用于對方法調用攔截以及回調
*
*/
public class CglibInterceptor implements MethodInterceptor {
   /**
    * CGLIB 增強類對象,代理類對象是由 Enhancer 類創建的,
    * Enhancer 是 CGLIB 的字節碼增強器,可以很方便的對類進行拓展
    */
   private Enhancer enhancer = new Enhancer();

   /**
    *
    * @param obj  被代理的對象
    * @param method 代理的方法
    * @param args 方法的參數
    * @param proxy CGLIB方法代理對象
    * @return  cglib生成用來代替Method對象的一個對象,使用MethodProxy比調用JDK自身的Method直接執行方法效率會有提升
    * @throws Throwable
    */
   @Override
   public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
       System.out.println("方法調用之前");
       Object o = proxy.invokeSuper(obj, args);
       System.out.println("方法調用之后");
       return o;
   }


   /**
    * 使用動態代理創建一個代理對象
    * @param c
    * @return
    */
   public  Object newProxyInstance(Class<?> c) {
       /**
        * 設置產生的代理對象的父類,增強類型
        */
       enhancer.setSuperclass(c);
       /**
        * 定義代理邏輯對象為當前對象,要求當前對象實現 MethodInterceptor 接口
        */
       enhancer.setCallback(this);
       /**
        * 使用默認無參數的構造函數創建目標對象,這是一個前提,被代理的類要提供無參構造方法
        */
       return enhancer.create();
   }
}

//測試類
public class MainCglibProxy {
    public static void main(String[] args) {
        CglibProxy cglibProxy = new CglibProxy();
        CglibHelloClass cglibHelloClass = (CglibHelloClass) cglibProxy.newProxyInstance(CglibHelloClass.class);
        cglibHelloClass.sayHello("isole");
        cglibHelloClass.sayByeBye("sss");
    }
}

對于需要被代理的類,它只是動態生成一個子類以覆蓋非 final 的方法,同時綁定鉤子回調自定義的攔截器。值得說的是,它比 JDK 動態代理還要快。值得注意的是,我們傳入目標類作為代理的父類。不同于 JDK 動態代理,我們不能使用目標對象來創建代理。目標對象只能被 CGLIB 創建。在例子中,默認的無參構造方法被使用來創建目標對象。

總結

靜態代理比較容易理解, 需要被代理的類和代理類實現自同一個接口, 然后在代理類中調用真正實現類, 并且靜態代理的關系在編譯期間就已經確定了。而動態代理的關系是在運行期間確定的。靜態代理實現簡單,適合于代理類較少且確定的情況,而動態代理則給我們提供了更大的靈活性。

JDK 動態代理所用到的代理類在程序調用到代理類對象時才由 JVM 真正創建,JVM 根據傳進來的 業務實現類對象 以及 方法名 ,動態地創建了一個代理類的 class 文件并被字節碼引擎執行,然后通過該代理類對象進行方法調用。我們需要做的,只需指定代理類的預處理、調用后操作即可。

靜態代理和動態代理都是基于接口實現的, 而對于那些沒有提供接口只是提供了實現類的而言, 就只能選擇 CGLIB 動態代理了

JDK 動態代理和 CGLIB 動態代理的區別

JDK 動態代理基于 Java 反射機制實現, 必須要實現了接口的業務類才能用這種方法生成代理對象。

CGLIB 動態代理基于 ASM 框架通過生成業務類的子類來實現。

JDK 動態代理的優勢是最小化依賴關系,減少依賴意味著簡化開發和維護并且有 JDK 自身支持。還可以平滑進行 JDK 版本升級,代碼實現簡單。基于 CGLIB 框架的優勢是無須實現接口,達到代理類無侵入,我們只需操作我們關系的類,不必為其它相關類增加工作量,性能比較高。

描述代理的幾種實現方式? 分別說出優缺點?

代理可以分為 "靜態代理" 和 "動態代理",動態代理又分為 "JDK 動態代理" 和 "CGLIB 動態代理" 實現。

靜態代理:代理對象和實際對象都繼承了同一個接口,在代理對象中指向的是實際對象的實例,這樣對外暴露的是代理對象而真正調用的是 Real Object.

優點:可以很好的保護實際對象的業務邏輯對外暴露,從而提高安全性。*

缺點:不同的接口要有不同的代理類實現,會很冗余

JDK 動態代理:
為了解決靜態代理中,生成大量的代理類造成的冗余;
JDK 動態代理只需要實現 InvocationHandler 接口,重寫 invoke 方法便可以完成代理的實現,

jdk 的代理是利用反射生成代理類 Proxyxx.class 代理類字節碼,并生成對象
jdk 動態代理之所以只能代理接口是因為代理類本身已經 extends 了 Proxy,而 java 是不允許多重繼承的,但是允許實現多個接口

優點:解決了靜態代理中冗余的代理實現類問題。

缺點:JDK 動態代理是基于接口設計實現的,如果沒有接口,會拋異常。

CGLIB 代理:
由于 JDK 動態代理限制了只能基于接口設計,而對于沒有接口的情況,JDK 方式解決不了;
CGLib 采用了非常底層的字節碼技術,其原理是通過字節碼技術為一個類創建子類,并在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯,來完成動態代理的實現。
實現方式實現 MethodInterceptor 接口,重寫 intercept 方法,通過 Enhancer 類的回調方法來實現。

但是 CGLib 在創建代理對象時所花費的時間卻比 JDK 多得多,所以對于單例的對象,因為無需頻繁創建對象,用 CGLib 合適,反之,使用 JDK 方式要更為合適一些。
同時,由于 CGLib 由于是采用動態創建子類的方法,對于 final 方法,無法進行代理。

優點:沒有接口也能實現動態代理,而且采用字節碼增強技術,性能也不錯。
缺點:技術實現相對難理解些。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374