徒手?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)主要步驟就是:
- 先檢查是否已經(jīng)加載了胜茧,即調(diào)用
findLoadedClass(cn)
,這是一個(gè)緩存機(jī)制仇味; - 檢查該類(lèi)所在的
module
是否被加載了呻顽,這是在java9引入module
之后出來(lái)的。如果找到了module
丹墨,那么就用這個(gè)module
來(lái)加載廊遍; - 委托給父加載器。這就是雙親委托模型的關(guān)鍵步驟贩挣;
- 嘗試自己加載喉前;
所以,FinalObject
這個(gè)類(lèi)會(huì)被加載很顯然是因?yàn)樗鼭M(mǎn)足了兩個(gè)條件:
- 在
findLoadedClass(cn)
調(diào)用中王财,返回null了卵迂。這很顯然,因?yàn)槲覀冏远x的加載器肯定沒(méi)有把加載的類(lèi)塞到AppClassLoader
的緩存里面绒净。 -
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)是需要被修改的惑灵,比如我們mock
的final
類(lèi);另外一類(lèi)是不需要修改的眼耀。
StupidMockClassLoader最終實(shí)現(xiàn)
最終我們的StupidMockClassLoader
是:
整體實(shí)現(xiàn)邏輯非常簡(jiǎn)單:
- 在構(gòu)造函數(shù)里面找到
PrepareForTest
注解英支,target
的值就是需要被自定義加載器加載的類(lèi); - 我們選擇重寫(xiě)
loadClass(name, resolve)
方法哮伟,以破壞雙親委托模型干花; -
loadClass
方法會(huì)嘗試從緩存里面獲取妄帘,獲取不到則判斷是否需要被自定義加載器加載,如果不需要池凄,則委托給parent
加載抡驼; - 如果需要被自己加載,那么判斷是否需要修改類(lèi)肿仑,調(diào)用
loadModifiedClass
或者loadUnmodifiedClass
; -
parent
在初始化的時(shí)候致盟,被設(shè)定為Thread.currentThread().getContextClassLoader()
。在這里是不會(huì)有問(wèn)題的尤慰; - 使用一個(gè)
ConcurrentMap
結(jié)合SoftReference
來(lái)做自身加載類(lèi)的緩存馏锡。可以確保的是伟端,如果一個(gè)類(lèi)沒(méi)有任何實(shí)例杯道,那么只會(huì)有一個(gè)這個(gè)Map
的SoftReference
指向它,這可以保證GC能夠正確處理類(lèi)责蝠; -
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)題支子;
- 在多線(xiàn)程環(huán)境下创肥,或者使用的框架自定義了
ClassLoader
,將parent
設(shè)置為Thread.currentThread().getContextClassLoader()
可能會(huì)有問(wèn)題值朋; - 每個(gè)單測(cè)類(lèi)都會(huì)創(chuàng)建一個(gè)
StupidMockClassLoader
實(shí)例叹侄,在并行測(cè)試的時(shí)候可能出現(xiàn)創(chuàng)建了大量ClassLoader
實(shí)例,從而快速達(dá)到metaspace
設(shè)置的上限昨登; - 不支持
SecurityManager
和ProtectionDomain
趾代,安全方面是一個(gè)大問(wèn)題; - 那些被忽略的包里面的類(lèi)丰辣,我們都沒(méi)有修改撒强,意味著無(wú)法
mock
其中的final
類(lèi);
最后說(shuō)的是笙什,到這一步飘哨,實(shí)際上mock
框架最關(guān)鍵的部分已經(jīng)出來(lái)了。采用自定義的Runner
和ClassLoader
我們幾乎可以做任何事情了琐凭,剩下不能做的事情芽隆,后面會(huì)在jvmagent
里面嘗試解決一下。
下一篇,我們將開(kāi)始討論when...then
胚吁,即方法的mock
牙躺。