源碼角度來看代理Proxy類

  • 近來在研究Retrofit的源碼拾徙,發(fā)現(xiàn)使用了動態(tài)代理的方式笙什;發(fā)現(xiàn)自己一直以來都是對這個方式一知半解飘哨,這次想要徹底的弄明白。

靜態(tài)代理

要想了解動態(tài)代理琐凭,首先要知道靜態(tài)代理芽隆,網(wǎng)上也有很多相關(guān)的文章,無非是說明相關(guān)的代碼怎么寫统屈,等等胚吁,看完好像是明白的,但是到了自己使用的時候你會感覺到其實(shí)自己還是不怎么懂愁憔;

那下面開始說一下靜態(tài)代理:

  • 先看一下靜態(tài)代理的類圖:


    image.png
  • 從類圖我們可以看到 SubjectProxy(代理類)需要代理目標(biāo)類(SubjectImp)腕扶,然后調(diào)目標(biāo)類中的方法;
    1. SubjectProxy(代理類)必須持有目標(biāo)類(SubjectImp)的引用;
    2. 外層首先調(diào)SubjectProxy類的callMethod()方法吨掌,然后在callMethod()方法內(nèi)調(diào)用目標(biāo)類(SubjectImp)的callMethod()方法半抱;
  • 因此不僅需要新建一個目標(biāo)類對象脓恕,同時還需要新建一個代理類對象;

代碼實(shí)例

嗯窿侈,看起來好像有點(diǎn)啰嗦进肯,那就代碼說話吧;

  • 假設(shè)現(xiàn)在有個學(xué)生接口棉磨,需要做作業(yè)和上英語課
public interface IStudent {
    void doHomework();
    void learnEnglish();
}
  • 我們來定義一個中學(xué)生類江掩,那么中學(xué)的學(xué)生是怎么做的呢?
public class MiddleSchoolStudent implements IStudent {
    @Override
    public void doHomework() {
        System.out.println("做中學(xué)作業(yè)");
    }
    @Override
    public void learnEnglish() {
        System.out.println("上中學(xué)的英語課");
    }
}

現(xiàn)在有一個需求乘瓤,需要計算做中學(xué)作業(yè)以及上英語課所花費(fèi)的時間是多少环形?

  • 那么你可以選擇直接在MiddleSchoolStudent 類種修改,但是可以能存在小學(xué)生類衙傀、大學(xué)生類抬吟,如果直接在類中修改,那么就需要修改很多地方统抬,就破壞了閉合原則火本;
  • 那應(yīng)該怎么做呢,可以使用使用一個時間計算的代理類聪建,還記得上面類圖嗎钙畔,代理類需要實(shí)現(xiàn)需要代理的接口;
public class TimeProxy implements IStudent {
    private final IStudent student;
    public TimeProxy(IStudent student) {
        this.student = student;
    }
    @Override
    public void doHomework() {
        TimeUtil.start("doHomework");
        student.doHomework();
        TimeUtil.finish("doHomework");
    }
    @Override
    public void learnEnglish() {
        student.learnEnglish();
    }
}
  • 然后通過代理類來做計算:
public static void main(String[] args) {
    IStudent midStu = new MiddleSchoolStudent();
    TimeProxy timeProxy = new TimeProxy(midStu);
    timeProxy.doHomework();
}

動態(tài)代理

  • 現(xiàn)在靜態(tài)代理講清楚了金麸,那下面來看看動態(tài)代理擎析,讓我們代碼先行。

代碼實(shí)例

使用的Java提供的Proxy類來創(chuàng)建動態(tài)代理挥下;

  • 首先需要實(shí)現(xiàn)InvocationHandler類揍魂,為什么不叫InvocationProxy呢?難道這個類不是代理類嗎棚瘟?
static class Invocation implements InvocationHandler {
    private final IStudent student;

    public Invocation(IStudent student) {
        this.student = student;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TimeUtil.start(method.getName());
        Object result = method.invoke(student, args);
        TimeUtil.finish(method.getName());
        return result;
    }
}
  • 然后我們在Main函數(shù)中調(diào)用方法Proxy來創(chuàng)建動態(tài)代理;
public static void main(String[] args) {
    IStudent midStu = new MiddleSchoolStudent();

    IStudent proxy = (IStudent) Proxy.newProxyInstance(
            IStudent.class.getClassLoader(),
            new Class[]{IStudent.class},
            new MidStudentProxy(midStu));
    proxy.doHomework();
}
  • 看到這里大家已經(jīng)有很多疑惑现斋,Proxy內(nèi)部是怎么做到的呢?為什么要實(shí)現(xiàn)InvocationHandler類偎蘸?

斷點(diǎn)追蹤

  • 下面讓我們通過斷點(diǎn)的方式來查詢Proxy.newProxyInstance究竟做了什么工作庄蹋?
  1. 首先我們點(diǎn)擊進(jìn)入newProxyInstance函數(shù),在內(nèi)部添加一個斷點(diǎn)禀苦,然后使用debug模式運(yùn)行蔓肯;


    newProxyInstance.png

    image.png
  2. 跟蹤下去,我們可以發(fā)現(xiàn)通過getProxyClass0() 方法振乏,產(chǎn)生了一個$Proxy0類蔗包;


    Proxy0.png
  • 這個$Proxy0是什么東西呢?
  1. 可以看到慧邮,這里獲取了$Proxy0類的構(gòu)造器调限,并且傳入的參數(shù)為InvocationHandler對象舟陆;
  • 在代碼最后調(diào)用了newInstance實(shí)例化了一個$Proxy0對象,h為我們創(chuàng)建的Invocation類耻矮;


    image.png
  1. 讓我們繼續(xù)追蹤秦躯,proxy.doHomework()方法的調(diào)用過程


    image.png
  • 可以發(fā)現(xiàn) proxy 是系統(tǒng)通過Proxy.newProxyInstance() 方法 生成的一個新的對象Proxy0,這個Proxy0類實(shí)現(xiàn)了IStudent接口裆装,包裹了一個我們傳進(jìn)去的Invocation對象踱承,而Invocation對象又持有了一個目標(biāo)類的引用;
  • 因此哨免,真正的代理類并不是Invocation茎活,而是$Proxy0這個系統(tǒng)生成的代理類;

$Proxy0 是如何產(chǎn)生的琢唾?

  • 那現(xiàn)在回到我們之前產(chǎn)生的問題载荔,$Proxy0這個類是如何生成的呢?

    • 其實(shí)上面已經(jīng)分析到了采桃,是通過 getProxyClass0() 這個方法來生成的懒熙,下面我們一起來看看這個方法。
    • 查看源碼發(fā)現(xiàn)Proxy類內(nèi)部有一個ProxyClassFactory類普办。
      • ProxyClassFactory.png
    • 繼續(xù)跟蹤工扎,忽略我們并不關(guān)心的功能,發(fā)現(xiàn)ProxyClassFactory內(nèi)部有一個方法泌豆,看注釋很明顯就是生成代理類操作,ProxyGenerator.generateProxyClass();
      • generateProxyClass.png
  • 下面我們來驗(yàn)證一下定庵,ProxyGenerator.generateProxyClass () 這個方法到底是不是如我們所猜想的,是通過它來生成代理類的呢踪危?

  • 我們可以把按照這個方法來生成對應(yīng)的代理類;
public static void main(String[] args) {
    IStudent midStu = new MiddleSchoolStudent();
    IStudent proxy = (IStudent) Proxy.newProxyInstance(
            IStudent.class.getClassLoader(),
            new Class[]{IStudent.class},
            new Invocation(midStu));

    proxy.doHomework();
    proxy.learnEnglish();

    byte[] bytes = ProxyGenerator.generateProxyClass(proxy.getClass().getSimpleName(), proxy.getClass().getInterfaces());
    try {
        // 保存到proxy.class 文件中
        FileOutputStream fileOutputStream = new FileOutputStream(new File("out/proxy.class"));
        fileOutputStream.write(bytes);
        fileOutputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 打開proxy.class文件猪落,讓我們來見識一下通過ProxyGenerator.generateProxyClass()生成的代理類的廬山真面目:
    • proxy.class.png
    • 可以看到贞远,這個代理類實(shí)現(xiàn)了我們定義的IStudent接口;

總結(jié)

  1. 也就是說笨忌,當(dāng)我們調(diào)用Proxy.newProxyInstance的時候蓝仲,它會根據(jù)傳遞的接口,來聲明對應(yīng)接口的代理類 $Proxy0 官疲;
  2. 我們在上面的分析已經(jīng)知道袱结,h 是創(chuàng)建$Proxy0 傳遞的Invocation類,可以看到在各個方法內(nèi)部都調(diào)用了invoke方法途凫;那如何調(diào)用invoke這就可以解釋的通了垢夹;
  3. 調(diào) Proxy0 的doHomework() 方法,實(shí)際上調(diào)用的是Proxy0代理類對象的doHomework()方法维费,此方法內(nèi)部持有Invocation對象果元,那實(shí)際是調(diào)用Invocation對象的invoke() 方法促王,然后再調(diào)用 Invocation內(nèi)部目標(biāo)類對象的learnEnglish() 方法;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末而晒,一起剝皮案震驚了整個濱河市蝇狼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌倡怎,老刑警劉巖迅耘,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異监署,居然都是意外死亡豹障,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門焦匈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來血公,“玉大人,你說我怎么就攤上這事缓熟±勰В” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵够滑,是天一觀的道長垦写。 經(jīng)常有香客問我,道長彰触,這世上最難降的妖魔是什么梯投? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮况毅,結(jié)果婚禮上分蓖,老公的妹妹穿的比我還像新娘。我一直安慰自己尔许,他們只是感情好么鹤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著味廊,像睡著了一般蒸甜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上余佛,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天柠新,我揣著相機(jī)與錄音,去河邊找鬼辉巡。 笑死恨憎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的红氯。 我是一名探鬼主播框咙,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咕痛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了喇嘱?” 一聲冷哼從身側(cè)響起茉贡,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎者铜,沒想到半個月后腔丧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡作烟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年愉粤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拿撩。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡衣厘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出压恒,到底是詐尸還是另有隱情影暴,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布探赫,位于F島的核電站型宙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伦吠。R本人自食惡果不足惜妆兑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毛仪。 院中可真熱鬧搁嗓,春花似錦、人聲如沸潭千。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刨晴。三九已至,卻和暖如春路翻,著一層夾襖步出監(jiān)牢的瞬間狈癞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工茂契, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝶桶,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓掉冶,卻偏偏與公主長得像真竖,于是被迫代替她去往敵國和親脐雪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354