徒手?jǐn)]一個(gè)Mock框架(三)—— JUnit4Runner+ClassLoader=為所欲為

徒手?jǐn)]一個(gè)Mock框架(一)——如何創(chuàng)建一個(gè)mock對(duì)象
徒手?jǐn)]一個(gè)Mock框架(二)——如何創(chuàng)建final類(lèi)的代理

本文代碼

徒手?jǐn)]一個(gè)Mock框架(二)——如何創(chuàng)建final類(lèi)的代理中窄坦,我們已經(jīng)知道位隶,為了mock一個(gè)final類(lèi)對(duì)象,我們需要自定義一個(gè)ClassLoader凭语。我們利用這個(gè)ClassLoader可以在讀入.class文件二進(jìn)制流的時(shí)候峭范,使用字節(jié)碼操作工具ASM去除類(lèi)定義中的final標(biāo)記位墓造。

然而在最后我們的類(lèi)加載器出現(xiàn)了一個(gè)小問(wèn)題:


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')

我們自定義的類(lèi)加載器加載了的類(lèi)杏愤,又被AppClassLoader加載器重新加載了一遍媒区。因此導(dǎo)致了ClassCastException驼仪。

今天我們就要解決這個(gè)問(wèn)題掸犬。

兩次加載的原因

首先我要解答一下為什么AppClassLoader會(huì)再一次加載我們已經(jīng)加載過(guò)的類(lèi)。

AppClassLoader雖然重寫(xiě)了loadClass方法绪爸,但是實(shí)際上只是做了一些安全方面的工作湾碎,最終的工作都是委托給父類(lèi)的實(shí)現(xiàn)來(lái)完成的。

AppClassLoader的父類(lèi)是BuildInClassLoader毡泻,其loadClass方法的核心邏輯在loadClassOrNull

該實(shí)現(xiàn)主要步驟就是:

  1. 先檢查是否已經(jīng)加載了胜茧,即調(diào)用findLoadedClass(cn),這是一個(gè)緩存機(jī)制仇味;
  2. 檢查該類(lèi)所在的module是否被加載了呻顽,這是在java9引入module之后出來(lái)的。如果找到了module丹墨,那么就用這個(gè)module來(lái)加載廊遍;
  3. 委托給父加載器。這就是雙親委托模型的關(guān)鍵步驟贩挣;
  4. 嘗試自己加載喉前;

所以,FinalObject這個(gè)類(lèi)會(huì)被加載很顯然是因?yàn)樗鼭M(mǎn)足了兩個(gè)條件:

  1. findLoadedClass(cn)調(diào)用中王财,返回null了卵迂。這很顯然,因?yàn)槲覀冏远x的加載器肯定沒(méi)有把加載的類(lèi)塞到AppClassLoader的緩存里面绒净。
  2. FinalObject.class文件處于AppClassLoader的類(lèi)路徑里面见咒。這也不難想到,因?yàn)樵谀J(rèn)情況下挂疆,IDE會(huì)把編譯后的文件都加入類(lèi)路徑里面改览。

但是這只解釋了為什么AppClassLoader會(huì)加載,但是沒(méi)有解釋?zhuān)瑸槭裁磿?huì)觸發(fā)這個(gè)類(lèi)加載器來(lái)加載FinalObject缤言。

為何會(huì)觸發(fā)AppClassLoader加載類(lèi)宝当?

很多人都沒(méi)有思考過(guò)這么一個(gè)問(wèn)題,JVM依據(jù)什么來(lái)決定使用哪個(gè)類(lèi)加載器胆萧。

這并不是說(shuō)雙親委托模型里面庆揩,JVM如何決定;而是指鸳碧,當(dāng)我有好幾個(gè)類(lèi)加載器的時(shí)候盾鳞,如果它們不構(gòu)成雙親委托模型,那么JVM該如何決定使用哪個(gè)類(lèi)加載器瞻离?

就如同我們實(shí)現(xiàn)的StupidMockClassLoader腾仅。它就是破壞了雙親委托模型。那么JVM什么情況下會(huì)使用StupidClassLoader來(lái)加載一個(gè)類(lèi)套利,而又在什么情況下使用預(yù)定義的——如AppClassLoader——加載器來(lái)加載類(lèi)呢推励?

答案也是很簡(jiǎn)單的:JVM會(huì)使用當(dāng)前類(lèi)加載器來(lái)加載依賴(lài)的類(lèi)鹤耍。

即,如果A里面創(chuàng)建了一個(gè)B的對(duì)象验辞,那么加載B的時(shí)候稿黄,就會(huì)使用加載A的類(lèi)加載器。

所以跌造,我們使用Class.forName來(lái)加載FinalObject類(lèi)的時(shí)候杆怕,并沒(méi)有改變當(dāng)前類(lèi)StupidMockClassLoaderTest的類(lèi)加載器,它依舊是AppClassLoader壳贪。

當(dāng)執(zhí)行到FinalObject object = (FinalObject) finalObjectClass.getConstructor().newInstance();的時(shí)候陵珍,JVM就會(huì)使用AppClassLoader來(lái)加載FinalObject

這幅圖揭示了這種關(guān)系违施。ClassA就相當(dāng)于我們的StupidMockClassLoaderTest互纯,ClassB就相當(dāng)于FinalObject。不同類(lèi)加載器加載的類(lèi)磕蒲,在JVM層面上就是兩個(gè)類(lèi)留潦。

這樣一來(lái),依據(jù)這種傳遞關(guān)系辣往,我們需要找到觸發(fā)加載StupidMockClassLoaderTest的類(lèi)兔院,再找到觸發(fā)加載這個(gè)類(lèi)的類(lèi)……一直回溯,直到找到最頂級(jí)的入口站削。

JUnit的頂級(jí)入口

因?yàn)槲覀冞@個(gè)StupidMock是給單測(cè)用的秆乳,所以實(shí)際上所謂的頂級(jí)入口,也就是單測(cè)的入口钻哩。考慮到單測(cè)框架有很多肛冶,我們這里只挑選junit4來(lái)做示例街氢。畢竟,這個(gè)會(huì)了睦袖,別的單測(cè)框架珊肃,也就是依葫蘆畫(huà)瓢的事情。

對(duì)于tomcat之類(lèi)的框架來(lái)說(shuō)馅笙,頂級(jí)入口也就是啟動(dòng)入口伦乔。

junit里面,提供了這種頂級(jí)入口董习,即Runner的概念烈和。簡(jiǎn)單來(lái)說(shuō),一個(gè)runner就是一個(gè)“容器”皿淋,或者說(shuō)是“上下文”招刹,或者說(shuō)“enhancer”恬试。開(kāi)發(fā)人員可以在runner里面隨心所欲擴(kuò)展各種奇怪的邏輯。

本質(zhì)上來(lái)說(shuō)疯暑,如果我們沒(méi)有自定義runner训柴,那么單測(cè)就會(huì)運(yùn)行在junit內(nèi)置的幾個(gè)runner之上。

我們要做的就是實(shí)現(xiàn)一個(gè)自己的runner妇拯,然后在runner里面使用自定義的classloader來(lái)加載所有測(cè)試類(lèi)幻馁。

其大概用法看起來(lái)是這樣的:



現(xiàn)在我們就要考慮一下這個(gè)StupidMockJunit4Runner該怎么實(shí)現(xiàn)了。

StupidMockJunit4Runner

StupidMockJunit4Runner的父類(lèi)有一個(gè)接收Class對(duì)象的構(gòu)造函數(shù)越锈,看上去就是最佳的切入點(diǎn)仗嗦。

我們可以在調(diào)用super(klass)的時(shí)候不再傳入原始的Class對(duì)象,而是傳入我們修改后的類(lèi)瞪浸。

按照我們前面的分析儒将,這樣大概就可以。然而現(xiàn)實(shí)是殘酷的对蒲,運(yùn)行測(cè)試我們得到的是java.lang.Exception: No runnable methods钩蚊。

這就是神奇了,因?yàn)槲覀兠髅髟跍y(cè)試方法上加了Test注解蹈矮。

為什么呢砰逻?

要記住,JVM類(lèi)型匹配是連ClassLoader一起考慮進(jìn)去的泛鸟。顯然蝠咆,StupidMockJunit4Runner是被AppClassLoader加載的,我們可以通過(guò)斷點(diǎn)進(jìn)去看:

而我們的StupidMockClassLoaderTest卻是被我們的StupidClassLoader加載的北滥。StupidMockJunit4Runner在查找測(cè)試方法的時(shí)候刚操,找的是被AppClassLoader加載的Test注解標(biāo)記的方法。

StupidMockClassLoaderTest里面的方法是被StupidMockClassLoader加載的Test注解所標(biāo)記的再芋。這就是出現(xiàn)java.lang.Exception: No runnable methods的原因菊霜。

那么,我們?cè)撛趺唇鉀Q這個(gè)問(wèn)題呢济赎?
答案是我們依舊使用自定義的類(lèi)加載器鉴逞,但是這個(gè)加載器不再是不分青紅皂白全部自己加載。我們給它加上一條規(guī)則:如果是需要被處理的司训,那么我們就使用自定義的加載器進(jìn)行加載构捡,否則我們就使用系統(tǒng)預(yù)定義的加載器進(jìn)行加載

改進(jìn)版StupidMockClassLoader

為了繼續(xù)下去壳猜,我先把測(cè)試貼出來(lái):


我們的目標(biāo)是使得mockFinal能夠通過(guò)勾徽。

實(shí)現(xiàn)的關(guān)鍵是,如何斷定一個(gè)類(lèi)需要被自定義加載器加載蓖谢,還是委托給系統(tǒng)加載器加載捂蕴?

首先我們可以肯定的是譬涡,被測(cè)試的那個(gè)類(lèi),在這里也就是StupidMockTest肯定要被自定義的加載器StupidMockClassLoader加載啥辨。不然的話(huà)涡匀,系統(tǒng)就會(huì)使用AppClassLoader來(lái)加載。這就造成了StupidMockTest里面的類(lèi)溉知,都會(huì)被AppClassLoader所加載陨瘩。這意味著,整個(gè)過(guò)程都不會(huì)經(jīng)過(guò)我們的自定義的加載器级乍。

其次我們還可以確定的是舌劳,FinalObject這個(gè)類(lèi)必然要被StupidMockClassLoader所加載,畢竟我們需要需改這個(gè)類(lèi)定義玫荣。

最后甚淡,我們可以斷定的是,系統(tǒng)類(lèi)——即JDK里面的那些捅厂,Junit的類(lèi)都不能被StupidMockClassLoader所加載贯卦。暫時(shí)我們也無(wú)法將所有的不需要被夾在的類(lèi)都列出來(lái),不過(guò)我們的目標(biāo)只是demo一下焙贷,所以可以先隨便寫(xiě)一點(diǎn)撵割。

StupidMockClassLoader里面,我們很容易知道StupidMockTest這個(gè)類(lèi)是頂級(jí)類(lèi)辙芍,因?yàn)闃?gòu)造函數(shù)里面?zhèn)鬟f進(jìn)來(lái)的就是這個(gè)類(lèi)啡彬。那么問(wèn)題是,我們?cè)趺粗?code>FinalObject這個(gè)類(lèi)需要被修改呢故硅?

答案是庶灿,我們需要用戶(hù)告訴我們。因此我們定義一個(gè)注解PrepareForTest吃衅。這個(gè)注解的概念就是從PowerMock里面借鑒來(lái)的跳仿,兩者的語(yǔ)義實(shí)際上差不多。

所以我們的單測(cè)形如:


有了這個(gè)注解之后捐晶,我們需要進(jìn)一步區(qū)分。被StupidMockClassLoader加載的類(lèi)妄辩,也要分成兩類(lèi):一類(lèi)是需要被修改的惑灵,比如我們mockfinal類(lèi);另外一類(lèi)是不需要修改的眼耀。

StupidMockClassLoader最終實(shí)現(xiàn)

最終我們的StupidMockClassLoader是:

整體實(shí)現(xiàn)邏輯非常簡(jiǎn)單:

  1. 在構(gòu)造函數(shù)里面找到PrepareForTest注解英支,target的值就是需要被自定義加載器加載的類(lèi);
  2. 我們選擇重寫(xiě)loadClass(name, resolve)方法哮伟,以破壞雙親委托模型干花;
  3. loadClass方法會(huì)嘗試從緩存里面獲取妄帘,獲取不到則判斷是否需要被自定義加載器加載,如果不需要池凄,則委托給parent加載抡驼;
  4. 如果需要被自己加載,那么判斷是否需要修改類(lèi)肿仑,調(diào)用loadModifiedClass或者loadUnmodifiedClass;
  5. parent在初始化的時(shí)候致盟,被設(shè)定為Thread.currentThread().getContextClassLoader()。在這里是不會(huì)有問(wèn)題的尤慰;
  6. 使用一個(gè)ConcurrentMap結(jié)合SoftReference來(lái)做自身加載類(lèi)的緩存馏锡。可以確保的是伟端,如果一個(gè)類(lèi)沒(méi)有任何實(shí)例杯道,那么只會(huì)有一個(gè)這個(gè)MapSoftReference指向它,這可以保證GC能夠正確處理類(lèi)责蝠;
  7. ALWAYS_IGNORE_PACKAGE定義的是需要被忽略的類(lèi)縮在的包的前綴党巾,這些類(lèi)應(yīng)該被系統(tǒng)所加載,就不需要碰了玛歌;

存在問(wèn)題

這個(gè)實(shí)現(xiàn)昧港,說(shuō)白了就是一個(gè)玩具實(shí)現(xiàn)。在真正用于生產(chǎn)環(huán)境的時(shí)候會(huì)有很多的問(wèn)題支子;

  1. 在多線(xiàn)程環(huán)境下创肥,或者使用的框架自定義了ClassLoader,將parent設(shè)置為Thread.currentThread().getContextClassLoader()可能會(huì)有問(wèn)題值朋;
  2. 每個(gè)單測(cè)類(lèi)都會(huì)創(chuàng)建一個(gè)StupidMockClassLoader實(shí)例叹侄,在并行測(cè)試的時(shí)候可能出現(xiàn)創(chuàng)建了大量ClassLoader實(shí)例,從而快速達(dá)到metaspace設(shè)置的上限昨登;
  3. 不支持SecurityManagerProtectionDomain趾代,安全方面是一個(gè)大問(wèn)題;
  4. 那些被忽略的包里面的類(lèi)丰辣,我們都沒(méi)有修改撒强,意味著無(wú)法mock其中的final類(lèi);

最后說(shuō)的是笙什,到這一步飘哨,實(shí)際上mock框架最關(guān)鍵的部分已經(jīng)出來(lái)了。采用自定義的RunnerClassLoader我們幾乎可以做任何事情了琐凭,剩下不能做的事情芽隆,后面會(huì)在jvmagent里面嘗試解決一下。

下一篇,我們將開(kāi)始討論when...then胚吁,即方法的mock牙躺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腕扶,隨后出現(xiàn)的幾起案子孽拷,更是在濱河造成了極大的恐慌,老刑警劉巖蕉毯,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乓搬,死亡現(xiàn)場(chǎng)離奇詭異框喳,居然都是意外死亡匹层,警方通過(guò)查閱死者的電腦和手機(jī)喇伯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)瑟俭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)夯膀,“玉大人子檀,你說(shuō)我怎么就攤上這事庄吼『顺耄” “怎么了乘瓤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵环形,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我衙傀,道長(zhǎng)抬吟,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任统抬,我火速辦了婚禮火本,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聪建。我一直安慰自己钙畔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布金麸。 她就那樣靜靜地躺著擎析,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挥下。 梳的紋絲不亂的頭發(fā)上揍魂,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音棚瘟,去河邊找鬼愉烙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛解取,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播返顺,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼禀苦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蔓肯!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起振乏,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蔗包,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后慧邮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體调限,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年误澳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耻矮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忆谓,死狀恐怖裆装,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倡缠,我是刑警寧澤哨免,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站昙沦,受9級(jí)特大地震影響琢唾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盾饮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一采桃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丐谋,春花似錦芍碧、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吏饿,卻和暖如春踪危,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猪落。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工贞远, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笨忌。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓蓝仲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子袱结,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355