徒手?jǐn)]一個Mock框架(二)——如何創(chuàng)建final類的代理

上一篇中阔加,我們還留下一個很復(fù)雜的問題,即如何創(chuàng)建一個final類的代理满钟。

對于一般的類來說胜榔,我們使用cglib來創(chuàng)建代理的時候,只需要調(diào)用setSuperClass便足夠了湃番。然而夭织,如果將一個final類作為參數(shù)傳遞過去,那么會拋出一個IllegalArgumentException: Cannot subclass final class吠撮。

要解決這個問題尊惰,途徑并不多。final作為一個非常關(guān)鍵的關(guān)鍵字,Java和Java虛擬機(jī)會在編譯器和運(yùn)行期都進(jìn)行檢查弄屡。以確保并不會有某個類是一個final類的子類题禀。

好在,我們可以使用一些小伎倆琢岩。要知道的一點(diǎn)是投剥,JVM只有在加載了類之后,才會知道這是一個final類担孔。所以江锨,我們要做的就是,在類完成加載之前糕篇,將一個final類修改為非final類啄育。

沒錯,這就是要用到自定義類加載技術(shù)了拌消。

ClassLoader

一般而言挑豌,如果要實(shí)現(xiàn)自定義的ClassLoader,只需要繼承ClassLoader這個抽象類墩崩,而后重寫findClass方法就可以氓英。并且在findClass里面調(diào)用defineClass方法。

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // todo
        // byte[] bytes = new byte[];
        return defineClass(name, bytes, 0, bytes.length);
    }
}

實(shí)現(xiàn)ClassLoader并不難鹦筹,難的是如何修改铝阐。

ASM

在Java里面,有一類工具叫做字節(jié)碼操作工具铐拐。這一類工具常見于復(fù)雜框架中徘键。

使用這一類工具一般有兩個理由:

  1. 一些信息只有在運(yùn)行期才能拿到,例如生成各種代理的框架遍蟋;
  2. 另外一個理由就是吹害,表達(dá)一些Java語言無法表達(dá)的語義。常見于各種基于JVM語言的編譯工具里面虚青;

這一次我們使用ASM工具它呀。ASM和其余的諸如Javaassis, BCEL比起來,具有抽象層次高挟憔,功能完備的特點(diǎn)钟些,美中不足的是,使用這個ASM工具要求對Java的字節(jié)碼指令和.class文件格式有較深的理解绊谭。cglib也是一種字節(jié)碼操作工具政恍,但它是在ASM基礎(chǔ)上的更高一級的抽象。

這一次我們只使用ASM里面的event based API达传。event based API的核心是實(shí)現(xiàn)一個類篙耗,并且繼承ClassVisitor迫筑。

抽象類ClassVisitor其實(shí)并沒有抽象方法。它里面的實(shí)現(xiàn)宗弯,就是按照讀取的字節(jié)碼流脯燃,原樣返回。ClassVisitor是依照Visitor模式設(shè)計(jì)的蒙保,所以我們可以實(shí)現(xiàn)多個ClassVisitor以完成不同的功能辕棚。

這一次我們只需要實(shí)現(xiàn)一個就夠了。這個實(shí)現(xiàn)的ClassVisitor非常簡單邓厕,在讀取.class文件的時候逝嚎,將類的final標(biāo)記去掉。

因此我們只需要重寫:


這個方法有一大堆參數(shù)详恼,不過我們只需要關(guān)注access參數(shù)补君。因?yàn)檫@個參數(shù)代表的就是一個類的“訪問性”。通過閱讀文檔我們能知道昧互,final是由16所代表的挽铁,二進(jìn)制的話則是10000。常量org.objectweb.asm.Opcodes#ACC_FINAL恰好代表這個敞掘。

所以我們重寫的地方很簡單:

public class RemoveFinalFlagClassVisitor extends ClassVisitor {

    public RemoveFinalFlagClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM7, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        // we have the final flag
        if((access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) {
            //remove the final flag
            access = access ^ Opcodes.ACC_FINAL;
        }
        super.visit(version, access, name, signature, superName, interfaces);
    }
}

代碼實(shí)際上很簡單叽掘,只不過是用位運(yùn)算了實(shí)現(xiàn)而已。

在調(diào)用super.visit的時候玖雁,我們就已經(jīng)把final關(guān)鍵字去掉了够掠。

ClassLoader的實(shí)現(xiàn)

這段代碼也不難。ClassWriter自身也繼承了ClassVisitor茄菊,所以實(shí)際上它和我們寫的RemoveFinalFlagClassVisitor構(gòu)成了一個Visitor鏈。

另外一個地方就是構(gòu)造函數(shù)里面赊堪,我調(diào)用的是super(null)面殖,這只是為了將類加載器的parent設(shè)置為null,從而避免在loadClass的類先被父加載器加載了——如果被父加載器加載哭廉,我們就無法去掉final了脊僚,除非這個類不在父加載器的classpath里面。

換句話來說遵绰,這種實(shí)現(xiàn)破壞了著名的雙親委托模型辽幌。

自己做的孽,含著淚也要頂住

于是我們會陷入另外一個問題椿访,在雙親委托模型被破壞之后乌企,我們的代碼會出現(xiàn)一個很微妙的問題:

這個單測居然失敗了,驚不驚喜成玫,意不意外加酵?拳喻?

而且失敗原因不是assertNotNull斷言失敗,而是第20行失敗了猪腕,異常是java.lang.ClassCastException!詳細(xì)的信息是:

java.lang.ClassCastException: class cn.com.flycash.stupidmock.testobj.FinalObject cannot be cast to class cn.com.flycash.stupidmock.testobj.FinalObject (cn.com.flycash.stupidmock.testobj.FinalObject is in unnamed module of loader cn.com.flycash.stupidmock.classloader.StupidMockClassLoader @50de0926; cn.com.flycash.stupidmock.testobj.FinalObject is in unnamed module of loader 'app')

也就是冗澈,在類型轉(zhuǎn)換的時候,虛擬機(jī)發(fā)現(xiàn)創(chuàng)建實(shí)例的類的加載器是我們自定義的StupidMockClassLoader陋葡,而要轉(zhuǎn)換到的類型雖然也是FinalObject亚亲,然而卻是使用AppClassLoader加載的。

我們可以在AppClassLoader#loadClass里面打上斷點(diǎn)腐缤,可以清楚看到捌归,在我們的加載器加載了一次FinalObject.class之后,AppClassLoader又加載了一次柴梆。

出現(xiàn)這個問題根源在于陨溅,從Java語言層面上來說,或者編譯層面上來說绍在,類的全限定名就代表了類的唯一性门扇,而在虛擬機(jī)層面上,類加載器+類全限定名才是類的唯一性偿渡。

這種割裂臼寄,在開發(fā)各種框架的時候,會坑死人溜宽。

直接原因再說我在使用IDE運(yùn)行測試的時候吉拳,FinalObject編譯后的文件FinalObject.class出現(xiàn)在了AppClassLoaderclasspath之下。

那么現(xiàn)在有兩個問題:

  1. 為什么JVM會觸發(fā)第二次加載适揉?畢竟按照理論上來說留攒,我們自定義的類加載器已經(jīng)加載到了這個類!
  2. 怎么解決這個問題嫉嘀?

這一章要解決的問題是“如何創(chuàng)建final類的代理”炼邀,到這里也算是創(chuàng)建出來了,只是又引出來新的問題剪侮。

新的問題且隨它去拭宁,下一篇我將解釋其中的一個問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓣俯,一起剝皮案震驚了整個濱河市杰标,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌彩匕,老刑警劉巖腔剂,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異驼仪,居然都是意外死亡桶蝎,警方通過查閱死者的電腦和手機(jī)驻仅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來登渣,“玉大人噪服,你說我怎么就攤上這事∈ぜ耄” “怎么了粘优?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呻顽。 經(jīng)常有香客問我雹顺,道長,這世上最難降的妖魔是什么廊遍? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任嬉愧,我火速辦了婚禮,結(jié)果婚禮上喉前,老公的妹妹穿的比我還像新娘没酣。我一直安慰自己,他們只是感情好卵迂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布裕便。 她就那樣靜靜地躺著,像睡著了一般见咒。 火紅的嫁衣襯著肌膚如雪偿衰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天改览,我揣著相機(jī)與錄音下翎,去河邊找鬼。 笑死宝当,一個胖子當(dāng)著我的面吹牛漏设,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播今妄,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鸳碧!你這毒婦竟也來了盾鳞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瞻离,失蹤者是張志新(化名)和其女友劉穎腾仅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體套利,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡推励,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年鹤耍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片验辞。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡稿黄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出跌造,到底是詐尸還是另有隱情杆怕,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布壳贪,位于F島的核電站陵珍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏违施。R本人自食惡果不足惜互纯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磕蒲。 院中可真熱鬧留潦,春花似錦、人聲如沸亿卤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽排吴。三九已至秆乳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钻哩,已是汗流浹背屹堰。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留街氢,地道東北人扯键。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像珊肃,于是被迫代替她去往敵國和親荣刑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

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

  • 1 基本信息 每個開發(fā)人員對java.lang.ClassNotFoundExcetpion這個異陈浊牵肯定都不陌生厉亏,...
    java小菜鳥閱讀 2,609評論 0 15
  • ClassLoader翻譯過來就是類加載器,普通的java開發(fā)者其實(shí)用到的不多烈和,但對于某些框架開發(fā)者來說卻非常常見...
    時待吾閱讀 1,076評論 0 1
  • 1爱只、classLoader 類加載器,將class文件加載到JVM虛擬機(jī)內(nèi)存中招刹,使得程序可以運(yùn)行恬试。通常情況下窝趣,JV...
    helloWorld_1118閱讀 2,215評論 0 2
  • 本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布 ClassLoader翻譯過來就是類加載器,普...
    尼爾君閱讀 661評論 1 0
  • 轉(zhuǎn)發(fā):本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布 ClassLoader翻譯過來就是類加載...
    尼爾君閱讀 535評論 0 1