Java提高班(六)反射和動態(tài)代理(JDK Proxy和Cglib)

反射和動態(tài)代理放有一定的相關(guān)性,但單純的說動態(tài)代理是由反射機(jī)制實現(xiàn)的鬼吵,其實是不夠全面不準(zhǔn)確的,動態(tài)代理是一種功能行為篮赢,而它的實現(xiàn)方法有很多齿椅。要怎么理解以上這句話琉挖,請看下文。

一涣脚、反射

反射機(jī)制是 Java 語言提供的一種基礎(chǔ)功能示辈,賦予程序在運行時<strong>自省</strong>(introspect,官方用語)的能力遣蚀。通過反射我們可以直接操作類或者對象矾麻,比如獲取某個對象的類定義,獲取類聲明的屬性和方法芭梯,調(diào)用方法或者構(gòu)造對象险耀,甚至可以運行時修改類定義。

1玖喘、獲取類(Class)對象

獲取類對象有三種方法:

  • 通過forName() -> 示例:Class.forName("PeopleImpl")
  • 通過getClass() -> 示例:new PeopleImpl().getClass()
  • 直接獲取.class -> 示例:PeopleImpl.class

2甩牺、類的常用方法

  • getName():獲取類完整方法;
  • getSuperclass():獲取類的父類累奈;
  • newInstance():創(chuàng)建實例對象贬派;
  • getFields():獲取當(dāng)前類和父類的public修飾的所有屬性;
  • getDeclaredFields():獲取當(dāng)前類(不包含父類)的聲明的所有屬性澎媒;
  • getMethod():獲取當(dāng)前類和父類的public修飾的所有方法赠群;
  • getDeclaredMethods():獲取當(dāng)前類(不包含父類)的聲明的所有方法;

更多方法:http://icdn.apigo.cn/blog/class-all-method.png

3旱幼、類方法調(diào)用

反射要調(diào)用類中的方法,需要通過關(guān)鍵方法“invoke()”實現(xiàn)的突委,方法調(diào)用也分為三種:

  • 靜態(tài)(static)方法調(diào)用
  • 普通方法調(diào)用
  • 私有方法調(diào)用

以下會分別演示柏卤,各種調(diào)用的實現(xiàn)代碼,各種調(diào)用的公共代碼部分匀油,如下:

// 此段代碼為公共代碼
interface People {
    int parentAge = 18;
    public void sayHi(String name);
}
class PeopleImpl implements People {
    private String privSex = "男";
    public String race = "漢族";
    @Override
    public void sayHi(String name) {
        System.out.println("hello," + name);
    }
    private void prvSayHi() {
        System.out.println("prvSayHi~");
    }
    public static void getSex() {
        System.out.println("18歲");
    }
}

3.1 靜態(tài)方法調(diào)用

// 核心代碼(省略了拋出異常的聲明)
public static void main(String[] args) {
    Class myClass = Class.forName("example.PeopleImpl");
    // 調(diào)用靜態(tài)(static)方法
    Method getSex = myClass.getMethod("getSex");
    getSex.invoke(myClass);
}

靜態(tài)方法的調(diào)用比較簡單缘缚,使用 getMethod(xx) 獲取到對應(yīng)的方法,直接使用 invoke(xx)就可以了敌蚜。

3.2 普通方法調(diào)用

普通非靜態(tài)方法調(diào)用桥滨,需要先獲取類示例,通過“newInstance()”方法獲取弛车,核心代碼如下:

Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method method = myClass.getMethod("sayHi",String.class);
method.invoke(object,"老王");

getMethod 獲取方法齐媒,可以聲明需要傳遞的參數(shù)的類型。

3.3 調(diào)用私有方法

調(diào)用私有方法纷跛,必須使用“getDeclaredMethod(xx)”獲取本類所有什么的方法喻括,代碼如下:

Class myClass = Class.forName("example.PeopleImpl");
Object object = myClass.newInstance();
Method privSayHi = myClass.getDeclaredMethod("privSayHi");
privSayHi.setAccessible(true); // 修改訪問限制
privSayHi.invoke(object);

除了“getDeclaredMethod(xx)”可以看出,調(diào)用私有方法的關(guān)鍵是設(shè)置 setAccessible(true) 屬性贫奠,修改訪問限制唬血,這樣設(shè)置之后就可以進(jìn)行調(diào)用了望蜡。

4、總結(jié)

1.在反射中核心的方法是 newInstance() 獲取類實例拷恨,getMethod(..) 獲取方法脖律,使用 invoke(..) 進(jìn)行方法調(diào)用,通過 setAccessible 修改私有變量/方法的訪問限制腕侄。

2.獲取屬性/方法的時候有無“Declared”的區(qū)別是小泉,帶有 Declared 修飾的方法或?qū)傩裕梢垣@取本類的所有方法或?qū)傩裕╬rivate 到 public)兜挨,但不能獲取到父類的任何信息膏孟;非 Declared 修飾的方法或?qū)傩裕荒塬@取 public 修飾的方法或?qū)傩园杌悖⒖梢垣@取到父類的信息柒桑,比如 getMethod(..)和getDeclaredMethod(..)。

二噪舀、動態(tài)代理

動態(tài)代理是一種方便運行時動態(tài)構(gòu)建代理魁淳、動態(tài)處理代理方法調(diào)用的機(jī)制,很多場景都是利用類似機(jī)制做到的与倡,比如用來包裝 RPC 調(diào)用界逛、面向切面的編程(AOP)。

實現(xiàn)動態(tài)代理的方式很多纺座,比如 JDK 自身提供的動態(tài)代理息拜,就是主要利用了上面提到的反射機(jī)制。還有其他的實現(xiàn)方式净响,比如利用傳說中更高性能的字節(jié)碼操作機(jī)制少欺,類似 ASM、cglib(基于 ASM)等馋贤。

動態(tài)代理解決的問題赞别?

首先,它是一個<strong>代理機(jī)制</strong>配乓。如果熟悉設(shè)計模式中的代理模式仿滔,我們會知道,代理可以看作是對調(diào)用目標(biāo)的一個包裝犹芹,這樣我們對目標(biāo)代碼的調(diào)用不是直接發(fā)生的崎页,而是通過代理完成。通過代理可以讓調(diào)用者與實現(xiàn)者之間<strong>解耦</strong>腰埂。比如進(jìn)行 RPC 調(diào)用实昨,通過代理,可以提供更加友善的界面盐固。還可以通過代理荒给,可以做一個全局的攔截器丈挟。

1、JDK Proxy 動態(tài)代理

JDK Proxy 是通過實現(xiàn) InvocationHandler 接口來實現(xiàn)的志电,代碼如下:

interface Animal {
    void eat();
}
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("The dog is eating");
    }
}
class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("The cat is eating");
    }
}

// JDK 代理類
class AnimalProxy implements InvocationHandler {
    private Object target; // 代理對象
    public Object getInstance(Object target) {
        this.target = target;
        // 取得代理對象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("調(diào)用前");
        Object result = method.invoke(target, args); // 方法調(diào)用
        System.out.println("調(diào)用后");
        return result;
    }
}

public static void main(String[] args) {
    // JDK 動態(tài)代理調(diào)用
    AnimalProxy proxy = new AnimalProxy();
    Animal dogProxy = (Animal) proxy.getInstance(new Dog());
    dogProxy.eat();
}

如上代碼曙咽,我們實現(xiàn)了通過動態(tài)代理,在所有請求之前和之后打印了一個簡單的信息挑辆。

注意: JDK Proxy 只能代理實現(xiàn)接口的類(即使是extends繼承類也是不可以代理的)例朱。

JDK Proxy 為什么只能代理實現(xiàn)接口的類?

這個問題要從動態(tài)代理的實現(xiàn)方法 newProxyInstance 源碼說起:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
// 省略其他代碼

來看前兩個源碼參數(shù)說明:

* @param   loader the class loader to define the proxy class
* @param   interfaces the list of interfaces for the proxy class to implement
  • loader:為類加載器鱼蝉,也就是 target.getClass().getClassLoader()
  • interfaces:接口代理類的接口實現(xiàn)列表

所以這個問題的源頭洒嗤,在于 JDK Proxy 的源碼設(shè)計。如果要執(zhí)意動態(tài)代理魁亦,非接口實現(xiàn)類就會報錯:

Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to xxx

2渔隶、Cglib 動態(tài)代理

JDK 動態(tài)代理機(jī)制只能代理實現(xiàn)了接口的類,Cglib 是針對類來實現(xiàn)代理的洁奈,他的原理是對指定的目標(biāo)類生成一個子類间唉,并覆蓋其中方法實現(xiàn)增強(qiáng),但因為采用的是繼承利术,所以不能對 final 修飾的類進(jìn)行代理呈野。

Cglib 可以通過 Maven 直接進(jìn)行版本引用,Maven 版本地址:https://mvnrepository.com/artifact/cglib/cglib

本文使用的是最新版本 3.2.9 的 Cglib印叁,在 pom.xml 添加如下引用:

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

Cglib 代碼實現(xiàn)被冒,如下:

class Panda {
    public void eat() {
        System.out.println("The panda is eating");
    }
}
class CglibProxy implements MethodInterceptor {
    private Object target; // 代理對象
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 設(shè)置父類為實例類
        enhancer.setSuperclass(this.target.getClass());
        // 回調(diào)方法
        enhancer.setCallback(this);
        // 創(chuàng)建代理對象
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("調(diào)用前");
        Object result = methodProxy.invokeSuper(o, objects); // 執(zhí)行方法調(diào)用
        System.out.println("調(diào)用后");
        return result;
    }
}

public static void main(String[] args) {
    // CGLIB 動態(tài)代理調(diào)用
    CglibProxy proxy = new CglibProxy();
    Panda panda = (Panda)proxy.getInstance(new Panda());
    panda.eat();
}

cglib 的調(diào)用通過實現(xiàn) MethodInterceptor 接口的 intercept 方法,調(diào)用 invokeSuper 進(jìn)行動態(tài)代理的轮蜕,可以直接對普通類進(jìn)行動態(tài)代理昨悼。

三、JDK Proxy VS Cglib

JDK Proxy 的優(yōu)勢:

  • 最小化依賴關(guān)系肠虽,減少依賴意味著簡化開發(fā)和維護(hù),JDK 本身的支持玛追,更加可靠税课;
  • 平滑進(jìn)行 JDK 版本升級,而字節(jié)碼類庫通常需要進(jìn)行更新以保證在新版上能夠使用痊剖;

Cglib 框架的優(yōu)勢:

  • 可調(diào)用普通類韩玩,不需要實現(xiàn)接口;
  • 高性能陆馁;

總結(jié): 需要注意的是找颓,我們在選型中,性能未必是唯一考量叮贩,可靠性击狮、可維護(hù)性佛析、編程工作量等往往是更主要的考慮因素,畢竟標(biāo)準(zhǔn)類庫和反射編程的門檻要低得多彪蓬,代碼量也是更加可控的寸莫,如果我們比較下不同開源項目在動態(tài)代理開發(fā)上的投入,也能看到這一點档冬。

本文所有示例代碼:https://github.com/vipstone/java-core-example.git

四膘茎、參考文檔

Java核心技術(shù)36講:http://t.cn/EwUJvWA

Java反射與動態(tài)代理:https://www.cnblogs.com/hanganglin/p/4485999.html


關(guān)注作者公眾號:

公眾號
公眾號

如果覺得本文對您有幫助,請我喝杯咖啡吧酷誓。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末披坏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子盐数,更是在濱河造成了極大的恐慌棒拂,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娘扩,死亡現(xiàn)場離奇詭異着茸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)琐旁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門涮阔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灰殴,你說我怎么就攤上這事敬特。” “怎么了牺陶?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵伟阔,是天一觀的道長。 經(jīng)常有香客問我掰伸,道長皱炉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任狮鸭,我火速辦了婚禮合搅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘歧蕉。我一直安慰自己灾部,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布惯退。 她就那樣靜靜地躺著赌髓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锁蠕,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天夷野,我揣著相機(jī)與錄音,去河邊找鬼匿沛。 笑死扫责,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逃呼。 我是一名探鬼主播鳖孤,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抡笼!你這毒婦竟也來了苏揣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤推姻,失蹤者是張志新(化名)和其女友劉穎平匈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藏古,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡增炭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拧晕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隙姿。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖厂捞,靈堂內(nèi)的尸體忽然破棺而出输玷,到底是詐尸還是另有隱情,我是刑警寧澤靡馁,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布欲鹏,位于F島的核電站,受9級特大地震影響臭墨,放射性物質(zhì)發(fā)生泄漏赔嚎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一胧弛、第九天 我趴在偏房一處隱蔽的房頂上張望尤误。 院中可真熱鬧,春花似錦叶圃、人聲如沸袄膏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春德崭,著一層夾襖步出監(jiān)牢的瞬間斥黑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工眉厨, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留锌奴,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓憾股,卻偏偏與公主長得像鹿蜀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子服球,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

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