溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

來自平行世界的救贖

發(fā)布時(shí)間:2020-06-25 21:58:16 來源:網(wǎng)絡(luò) 閱讀:409 作者:夢莊 欄目:軟件技術(shù)

神級的拷問來了

來自平行世界的救贖
為什么要說是救贖呢?先跟各位討論個(gè)“死亡問題”,如果你的女票或者你老婆問你,“我跟你媽落水了,你先救誰?”
來自平行世界的救贖
哈哈,沒錯(cuò),就是這個(gè)來之中國的古老聲音,這個(gè)拷問你內(nèi)心的世紀(jì)難題!怕了沒?
可以拋硬幣,也可以找個(gè)漁網(wǎng)一次性撈起來,可是等等,在這緊急關(guān)頭你真的有這么多時(shí)間?
此時(shí)的你肯定最想變成超人,或者修得絕世秘法“分身術(shù)”,這樣就不用做這道艱難的選擇題了。
平行宇宙論告訴我們,這世界有無數(shù)個(gè)copy,你也有無數(shù)個(gè)copy,只要找另外一個(gè)世界借一個(gè)你過來,你的內(nèi)心就能得到神圣的救贖了。
來自平行世界的救贖


怎么做

好了,假設(shè)真的由一個(gè)平行世界,為了保證這個(gè)方法落地的可行性,我們還需要保證

  • 平行世界的真的是我

    我代表著不僅僅是名字外貌,還有我的人生種種組成才是完整的我,最佳結(jié)果是什么?平行世界的我就是從這一刻跟我分離出來的,是我的真正copy,通一個(gè)版本的copy!

  • 另一個(gè)我能到這個(gè)世界來幫我救一個(gè)人

    這是我們討論這個(gè)問題的本質(zhì),解決不了的話,再來100個(gè)平行世界的我又能怎么樣?所以他要能干涉到我們這個(gè)世界,能在這個(gè)世界行動!可是既然是平行世界,那么肯定是無法過來的啊,怎么辦呢?大家都知道,作為高緯度的神可以投影到低緯度的世界中,通過“投影”來行動,或許我們可以這么干?

整理下思路
來自平行世界的救贖

兩個(gè)同樣的我同時(shí)救了兩個(gè)生命中最重要的人,不踩坑不扎心,再來幾個(gè)老婆都救得了,簡直完美!


圣者(程序猿)時(shí)間

解決哲學(xué)問題而內(nèi)心得到升華的我們,此時(shí)回歸真我(現(xiàn)實(shí)),利用僅存的圣者模式思考這個(gè)方法的現(xiàn)實(shí)意義。
“高大上”的工程師職業(yè)過程中,我們會遇上前人有意或者無意在代碼中留下的坑,譬如

  • 某些設(shè)計(jì)得很不合理的單例模式,這讓我們在一個(gè)JVM中只有一個(gè)實(shí)例存在
  • 將某些數(shù)據(jù)(如狀態(tài))存在靜態(tài)字段中,如果修改可能導(dǎo)致運(yùn)行出錯(cuò)
  • 或者其它蛋疼不考慮后來人的設(shè)計(jì)

但我們需要為這類對象創(chuàng)建全新一個(gè)實(shí)例去拯救世界時(shí),除了內(nèi)心被千萬草泥馬踐踏而過之外,似乎只能感受到這世界滿滿的惡意了。
不,肯定不是!
我們可是在圣者模式!!
在操蛋的現(xiàn)實(shí)社會中我們只是屌絲,但在0和1的世界里,我們可是神!
無所不能的神!
來自平行世界的救贖
神愛世人,怎么會讓自己的羔羊生活在水深火熱中呢?
就像拯救你媽你老婆和你內(nèi)心那樣,我們可以創(chuàng)造出一個(gè)平行世界出來啊,從虛無中造物不就是我們的本能么?


設(shè)計(jì)

前面我們已經(jīng)討論過世紀(jì)難題的解決方案,也給出了設(shè)計(jì)圖,此時(shí)的我們只要把這個(gè)思維轉(zhuǎn)換為由0和1組成的另一個(gè)世界的方式表達(dá),似乎就可以了?
來自平行世界的救贖
我們要通過救世主對象去操作一堆“待拯救的對象”,嗯,這就是救世主應(yīng)該做的。
但是,另外一邊出現(xiàn)災(zāi)難了,又有一堆“待拯救的對象”排排坐,等著救世主來拯救。
救世主說,臥槽,我TM分身乏術(shù)啊,上帝沒給我分身這個(gè)超能力,我也很無助啊。
來自平行世界的救贖
好了,這個(gè)時(shí)候就是英雄閃亮登場的機(jī)會啦。
來自平行世界的救贖
你爹媽不給你分身術(shù),咱不分身啦,咱直接開一個(gè)新的世界,拉一個(gè)過來唄,別問為啥,就是這么任性。
來自平行世界的救贖
嗯,具體操作就像如何把大象放進(jìn)冰箱一樣分3步

1、新開辟一個(gè)世界;
2、復(fù)制一個(gè)救世主過去;
3、把救世主投影過來;

步驟有啦,分析下怎么執(zhí)行。

  1. 新開辟一個(gè)世界

    我們是務(wù)實(shí)的工程師,不能吹逼,所以不應(yīng)該叫新開辟世界,應(yīng)該叫做制作一個(gè)相對比較隔離的環(huán)境出來,要求呢?這個(gè)環(huán)境應(yīng)該

    • 工作在里面的對象跟外面的能力應(yīng)該是完全一樣的
    • 環(huán)境外面應(yīng)該是無法感知里面的情況的
    • 環(huán)境內(nèi)外的對象應(yīng)該是完全不同的

    我們暫且為這個(gè)環(huán)境命名為“沙箱”(Sandbox)吧。
    以單例設(shè)計(jì)為參考,單例設(shè)計(jì)一般是寄托于類(Class)存在的,為了復(fù)制這個(gè)對象,我們需要做的是將整個(gè)Class復(fù)制一份。
    來自平行世界的救贖
    我們知道Java中的Class是由ClassLoader裝載進(jìn)內(nèi)存的,而ClassLoader采用的是雙親委派機(jī)制,一個(gè)ClassLoader內(nèi)獨(dú)有的業(yè)務(wù)對象對其它ClassLoader是不存在的,這不就完美滿足我們上面說的三個(gè)點(diǎn)嗎?Good,就它了!
    方案:采用ClassLoader作為沙箱環(huán)境隔離

  2. 復(fù)制一個(gè)救世主過去

    前面我們確定了ClassLoader方案后思路自然豁然開朗,現(xiàn)在考慮將Class復(fù)制進(jìn)沙箱(ClassLoader)內(nèi)就非常簡單啦!
    我們知道,ClassLoader裝載Class時(shí)候其實(shí)是讀取.class文件,再通過ClassLoader的defineClass來實(shí)際定義一個(gè)類的,嗯,那我們將沙箱外的類定義復(fù)制過來也可以這樣,兩步
    首先讀取.class內(nèi)容。這個(gè)文件在哪里呢?當(dāng)jar包被ClassLoader裝入內(nèi)存后,通過getResource就可以將文件數(shù)據(jù)讀取到啦,完美!
    在沙箱內(nèi)定義類。簡單,就一個(gè)defineClass,打完收工~
    嘿,別急,小心類重新定義哦,記得記錄下定義過哪些類。
    來自平行世界的救贖

  3. 把救世主投影過來

    對,這也是個(gè)問題。
    剛剛我們有說過,不同ClassLoader的獨(dú)有業(yè)務(wù)對象對其它ClassLoader而言是不存在的!這就引發(fā)出問題了,外面無法使用里面創(chuàng)造出來的對象實(shí)例!
    來自平行世界的救贖
    舉個(gè)例子

    BizObject biz = new BizObject(); //OK
    BizObject biz2 = Sandbox.createObject(BizObject.class); //出錯(cuò)

    為什么出錯(cuò)呢?因?yàn)樯诚鋬?nèi)外的BizObject是不一樣的啊,正反粒子在一起會湮滅的。。。
    所以我們需要投影。
    好吧,不是投影,我們需要有一個(gè)代理,在沙箱外培養(yǎng)一個(gè)傀儡,哦不是,是代理,對這個(gè)代理的所有操作都能反饋到沙箱內(nèi)去執(zhí)行。
    來自平行世界的救贖

嗯,到這里為止,我們基本將問題梳理一遍了,那么下一步。。。。。。
來自平行世界的救贖


神說,要有光

通過上面分析和梳理,我們基本已經(jīng)確定了方向和邏輯,現(xiàn)在呢,萬事俱備,只缺一道神奇的東風(fēng)我們就可以進(jìn)入全新世界里了,那我們開始擼代碼!
來自平行世界的救贖
等等這位同學(xué),我們是不是漏了什么?
擼代碼前我們先要進(jìn)行設(shè)計(jì)??!
來自平行世界的救贖
好吧,我們討論下本次需求。。。
首先,我們假定了已經(jīng)設(shè)定了一個(gè)神奇的“沙箱”,沙箱內(nèi)外隔離,所以內(nèi)外的通信只能通過一座也是非常神奇的橋梁來進(jìn)行,這就是“代理”;
當(dāng)外部的某位同學(xué)需要創(chuàng)建一個(gè)對象但又受到各種限制的時(shí)候,他可以在沙箱內(nèi)創(chuàng)建一個(gè)此對象的分身,然后通過分身的代理進(jìn)行操作就可以實(shí)現(xiàn)對分身的操縱,從而達(dá)成目的。
嗯,需求只有這么多,接下來我們談?wù)勗O(shè)計(jì)。
上面討論中我們決定了使用ClassLoader對沙箱內(nèi)外進(jìn)行隔離,可是不是直接暴露ClassLoader接口給外部使用呢?
ClassLoader能對底層類進(jìn)行操作,雖然功能強(qiáng)大,但操作復(fù)雜度高,一不留神容易出現(xiàn)問題,所以我們應(yīng)該對它進(jìn)行封裝,僅提供我們期望用戶去使用的接口,而且我們認(rèn)為它應(yīng)該具備這些特點(diǎn)

  • 功能單一
  • 與沙箱不相干的都不要暴露
  • 創(chuàng)建對象后直接可以使用

這對ClassLoader來說有些強(qiáng)人所難,所以我們需要把它隱藏起來,創(chuàng)造一個(gè)沙箱對外提供服務(wù),而將ClassLoader隱藏在沙箱內(nèi)部,假定它叫“SandboxClassLoader”。
這樣我們就有了

  • 調(diào)用者
  • 沙箱
  • SandboxClassLoader
  • 外部ClassLoader

四個(gè)對象了。
還有一點(diǎn),上面說過我們的調(diào)用者通過代理對沙箱內(nèi)對象進(jìn)行操作,還記得為什么要使用代理嗎?使用代理的本質(zhì)原因是沙箱內(nèi)外的類分屬不同ClassLoader,即使同名類也是不同的!
同樣道理,當(dāng)我們通過代理對象進(jìn)行調(diào)用時(shí),參數(shù)傳遞使用的是沙箱外的對象,進(jìn)入沙箱內(nèi)也是不能直接使用的,因此,我們同樣需要對這類對象進(jìn)行轉(zhuǎn)換。
此處我們僅考慮值對象參數(shù),各位同學(xué)如果關(guān)心其它對象傳參的話,需要進(jìn)行類似的代理轉(zhuǎn)換,但值對象的話,我們只要進(jìn)行值復(fù)制就行了,無需太過復(fù)雜處理
我們通過一幅圖來說明下這個(gè)關(guān)系
來自平行世界的救贖
圖片很直觀,就不再重復(fù)解說啦
嗯,基本梳理應(yīng)該已經(jīng)非常清晰了,圖中只有藍(lán)色的“沙箱內(nèi)某對象”屬于工作在沙箱內(nèi),動態(tài)創(chuàng)建出來的,其它都是在沙箱外;
而方框畫出了沙箱組件邊界,調(diào)用者和APPClassLoader都屬于已存在的實(shí)例無需關(guān)心,組件內(nèi)部就屬于需要實(shí)現(xiàn)的部分了。
列一下關(guān)鍵幾個(gè)類
來自平行世界的救贖
可以看出,Sandbox的API已經(jīng)變得非常單一和簡單了。
為了簡化設(shè)計(jì),這里規(guī)定了待創(chuàng)建的對象必須有無參構(gòu)造函數(shù),如果同學(xué)有需要通過有參構(gòu)造函數(shù)構(gòu)造對象的話,可以進(jìn)行擴(kuò)展實(shí)現(xiàn),歡迎一起做好這個(gè)沙箱工具
為什么這里要分開枚舉和非枚舉對象呢?有同學(xué)清楚嗎?
枚舉的概念是指能有限列舉出來的東西,在java中,枚舉對象繼承自Enum,不能通過new方法進(jìn)行構(gòu)造,只能從枚舉的值中選取
而對象繼承自O(shè)bject,大家都非常的熟悉

創(chuàng)世紀(jì)

終于進(jìn)入最重要的擼代碼環(huán)節(jié)了。。。
來自平行世界的救贖
挑重點(diǎn)的代碼出來,咱擼一擼

public class Sandbox {
    private SandboxClassLoader classLoader;
    private SandboxUtil util = new SandboxUtil();
    private List<String> redefinedPackages;

    public Sandbox(List<String> packages){
        redefinedPackages = packages;
        classLoader = new SandboxClassLoader(getContextClassLoader());
    }

    /**
     * 沙箱對象構(gòu)造方法
     * @param redefinedPackages 需工作在沙箱內(nèi)的包
     *                          此包下面所有類都在工作在沙箱內(nèi)
     */
    public Sandbox(String... redefinedPackages){
        this(Lists.newArrayList(redefinedPackages));
    }
    // ......
}

先說說構(gòu)造方法。
既然是沙箱對象,為什么要設(shè)計(jì)有參構(gòu)造方法呢?
實(shí)際使用中,我們會考慮某些類之間內(nèi)聚,當(dāng)一個(gè)類放在沙箱內(nèi)運(yùn)行時(shí),其它也建議放在沙箱內(nèi)跑,而我們學(xué)過“單一性原則”,知道一個(gè)包內(nèi)一般都是比較內(nèi)聚的,所以這里設(shè)計(jì)就是指定某些package路徑,沙箱將會對這些包內(nèi)對象進(jìn)行接管。
對于不在這些包內(nèi)的類,如果我們調(diào)用了沙箱來構(gòu)造會怎么樣呢?所謂“Talk is cheap, show me the code”~~
請稍后,我們繼續(xù)構(gòu)造函數(shù),哈哈~~這個(gè)問題我們標(biāo)記為問題1稍后討論
這里出現(xiàn)了SandboxClassLoader,使用了getContextClassLoader()作為參數(shù)傳遞,此處做了什么呢?我們先看看SandboxClassLoader的構(gòu)造方法

    /**
     * 沙箱隔離核心
     *
     * 通過ClassLoader將進(jìn)行類級別的運(yùn)行時(shí)隔離
     *
     * 此類本質(zhì)上是代理了currentContextClassLoader對象,并增加了對部分需要在沙箱內(nèi)運(yùn)行的類處理能力
     */
    class SandboxClassLoader extends ClassLoader{
        //當(dāng)前上下文的ClassLoader,用于尋找類實(shí)例并克隆進(jìn)沙箱
        private final ClassLoader contextClassLoader;
        //緩存已經(jīng)創(chuàng)建過的Class實(shí)例,避免重復(fù)定義
        private final Map<String, Class> cache = Maps.newHashMap();

        SandboxClassLoader(ClassLoader contextClassLoader) {
            this.contextClassLoader = contextClassLoader;
        }
        //......
    }

SandboxClassLoader的構(gòu)造方法僅僅是將傳入的contextClassLoader進(jìn)行暫存?zhèn)溆茫敲次覀冞€是看看getContextClassLoader方法

    /**
     * 獲取當(dāng)前上下文的類裝載器
     *
     * 此類裝載器需包含MQClient相關(guān)類定義
     * PS:單獨(dú)定義為一個(gè)方法,是擔(dān)心當(dāng)這個(gè)上下文類裝載器滿足不了要求時(shí)可以快速更換
     * @return 當(dāng)前類裝載器
     */
    private ClassLoader getContextClassLoader() {
        //從類裝載器機(jī)制而言,線程上下文的類轉(zhuǎn)載器是最符合要求的
        return Thread.currentThread().getContextClassLoader();
    }

好簡單??!
其實(shí)這里是有一些設(shè)計(jì)依據(jù)的:我們要去創(chuàng)建一個(gè)對象,那么這個(gè)對象的類定義必然在當(dāng)前代碼可訪問的。
基于這個(gè)考慮,我們可以確定,當(dāng)用戶使用類似A a = Sandbox.createObject(A.class)進(jìn)行創(chuàng)建沙箱內(nèi)對象時(shí),A類在這段代碼執(zhí)行的上下文必然可以訪問,此時(shí)我們可以通過此上下文的ClassLoader去獲取到這個(gè)A類對應(yīng)的.class資源文件,然后重定義該類了。
繼續(xù)看看相關(guān)代碼,為了閱讀方便,我重新組織了下代碼結(jié)構(gòu)

public class Sandbox {
    private SandboxClassLoader classLoader;
    //......

    /**
     * 在沙箱內(nèi)創(chuàng)建指定名稱的類實(shí)例
     *
     * 如該名稱類不屬于redefinedPackages所指定的包內(nèi),則直接返回外部類實(shí)例
     * @param clzName 待創(chuàng)建實(shí)例的類名稱
     * @return 指定類名稱的實(shí)例對象
     */
    public <T extends Object> T createObject(String clzName) throws ClassNotFoundException, SandboxCannotCreateObjectException {
        Class clz = Class.forName(clzName);
        return (T) createObject(clz);
    }

    /**
     * 在沙箱內(nèi)創(chuàng)建指定Class的實(shí)例
     * @param clz 待創(chuàng)建實(shí)例的Class
     * @return 跟clz功能相同并工作在沙箱內(nèi)的類實(shí)例
     */
    public synchronized <T extends Object> T createObject(Class<T> clz) throws SandboxCannotCreateObjectException {
        try {
            final Class<?> clzInSandbox = classLoader.loadClass(clz.getName());
            final Object objectInSandbox = clzInSandbox.newInstance();

            //如果對象的類裝載器和clz的類裝載器一致,說明不是需要工作在沙箱內(nèi)的對象,直接返回即可,無需代理
            if(objectInSandbox.getClass().getClassLoader() == clz.getClassLoader()){
                return (T) objectInSandbox;
            }

            /*
            創(chuàng)建生產(chǎn)者的代理:由于沙箱內(nèi)外的對象本質(zhì)上屬于不同的類,因此需要將兩者能力橋接起來
                            這里采用了代理模式,通過創(chuàng)建沙箱外的對象實(shí)例,并將其所有方法調(diào)用通過代理轉(zhuǎn)發(fā)到沙箱內(nèi)執(zhí)行
                            另外,由于沙箱內(nèi)外的所有實(shí)例都屬于不同的類,因此,對于參數(shù)和返回值還需要進(jìn)行對象轉(zhuǎn)換,將沙箱內(nèi)外的對象進(jìn)行對等克隆
             */

            //通過cglib創(chuàng)建對象的子類代理
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(clz);
            enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> {
                Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
                //調(diào)用前需對參數(shù)進(jìn)行克隆,轉(zhuǎn)換為沙箱內(nèi)對象
                Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
                Object result = targetMethod.invoke(objectInSandbox, targetArgs);
                //調(diào)用后續(xù)對結(jié)果進(jìn)行克隆,轉(zhuǎn)換為沙箱外對象
                return util.cloneTo(result, getContextClassLoader());
            });
            return (T) enhancer.create();
        }catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
            throw new SandboxCannotCreateObjectException("無法在沙箱內(nèi)創(chuàng)建對象", e);
        }
    }

    //......
}

Sandbox中創(chuàng)建對象的主要方法出現(xiàn)了!為了方便閱讀,我將無關(guān)代碼剔除,僅保留createObject方法。
T createObject(String clzName)方法無具體實(shí)現(xiàn),僅進(jìn)行參數(shù)clzName的校驗(yàn),然后就轉(zhuǎn)給T createObject(Class clz),因此主要分析這個(gè)方法。
其實(shí)代碼量不多(僅19行還包括各種花括號),主要都是注釋,脈絡(luò)如下

  1. 先獲取參數(shù)clz在沙箱內(nèi)的對于類定義clzInSandbox,并通過clzInSandboxnewInstance創(chuàng)建該類的一個(gè)具體實(shí)例objectInSandbox因此這里要求clz有無參構(gòu)造函數(shù)
  2. 判斷clzInSandbox是否運(yùn)行在沙箱內(nèi),如果不是運(yùn)行在沙箱內(nèi)的話,無需創(chuàng)建代理直接將對象objectInSandbox返回;
    為什么要做這個(gè)判斷嗯?這里可以順帶解答前面的問題1了,從代碼

    //如果對象的類裝載器和clz的類裝載器一致,說明不是需要工作在沙箱內(nèi)的對象,直接返回即可,無需代理
    if(objectInSandbox.getClass().getClassLoader() == &gt; clz.getClassLoader()){
        return (T) objectInSandbox;
    }

    我們可以看出來,當(dāng)創(chuàng)建出來的objectInSandbox也是運(yùn)行在外部的ClassLoader時(shí),其實(shí)是不去創(chuàng)建代理的,因?yàn)樗褪且粋€(gè)沙箱外的對象,又何必去創(chuàng)建代理這么多此一舉呢?
    可我們明明調(diào)用的是classLoader.loadClass(clz.getName())去取得沙箱內(nèi)的類定義,為什么得到的卻是沙箱外的呢?這跟我們對SandboxClassLoader這個(gè)類的設(shè)計(jì)是否矛盾了呢?
    好,去看看對應(yīng)的代碼,show me the code

    class SandboxClassLoader extends ClassLoader{
        //當(dāng)前上下文的ClassLoader,用于尋找類實(shí)例并克隆進(jìn)沙箱
        private final ClassLoader contextClassLoader;
        //......  
    
        /**
         * 覆蓋父類的轉(zhuǎn)載類進(jìn)內(nèi)存的方法
         * @param name 指定類名稱
         * @return 已轉(zhuǎn)載進(jìn)內(nèi)存的Class實(shí)例
         * @throws ClassNotFoundException
         */
        @Override
        public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {
            return findClass(name);
        }
    
        /**
         * 重定義類轉(zhuǎn)載邏輯
         *
         * 1、對于需要運(yùn)行在沙箱內(nèi)的類(redefinedPackages中聲明),通過復(fù)制contextClassLoader類定義的方式,直接運(yùn)行在此ClassLoader下
         * 2、對于不需要運(yùn)行在沙箱內(nèi)的類,直接返回上下文類定義,以減少資源占用
         * @param name 類名稱(全路徑)
         * @return 類定義
         */
        @Override
        protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
            if(isRedefinedClass(name)) {
                return getSandboxClass(name);
            } else {
                return contextClassLoader.loadClass(name);
            }
        }
    
        //......
    }

    看得出實(shí)際實(shí)現(xiàn)邏輯的代碼是findClass方法,僅幾句而已,翻譯過來就是“需要重定義的類我們從沙箱內(nèi)取得,不需要的直接從外部取”,所以會有對象的ClassLoader是外部的。
    那什么是“需要重定義的類”呢?

    /**
     * 是否需要運(yùn)行在沙箱內(nèi)的類
     * @param name 類名稱
     */
    boolean isRedefinedClass(String name) {
        //校驗(yàn)是否沙箱約定的需要重定義的包
        for (String redefinedPackage : redefinedPackages) {
            if(name.startsWith(redefinedPackage)){
                return true;
            }
        }
        return false;
    }

    只要是Sandbox類構(gòu)造時(shí)指定的包下面的類,統(tǒng)統(tǒng)都屬于需要重新在SandboxClassLoader中重定義的。

  3. 利用cglib庫創(chuàng)建objectInSandbox的代理對象,攔截該代理對象的所有方法執(zhí)行,全部轉(zhuǎn)去實(shí)際的對象objectInSandbox中執(zhí)行;
    cglib創(chuàng)建對象的代碼不分析了,本質(zhì)就是通過創(chuàng)建一個(gè)指定類的子類對方法進(jìn)行攔截的過程;
    我們關(guān)心的應(yīng)該是攔截器干了什么?

    enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -&gt; {
                Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
                //調(diào)用前需對參數(shù)進(jìn)行克隆,轉(zhuǎn)換為沙箱內(nèi)對象
                Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
                Object result = targetMethod.invoke(objectInSandbox, targetArgs);
                //調(diào)用后續(xù)對結(jié)果進(jìn)行克隆,轉(zhuǎn)換為沙箱外對象
                return util.cloneTo(result, getContextClassLoader());
            });

    我們會從沙箱內(nèi)的對象中取得同名同參的方法,然后將參數(shù)進(jìn)行轉(zhuǎn)換到沙箱內(nèi),再執(zhí)行沙箱內(nèi)對象方法并得到結(jié)果,最后還要將結(jié)果進(jìn)行轉(zhuǎn)換到沙箱外對象才返回;
    邏輯非常清晰,但沙箱內(nèi)外對象如何轉(zhuǎn)換呢?
    這里代碼有些長且無聊就不單獨(dú)貼出來了,有興趣的同學(xué)可以上github上自行下載,大體邏輯如下

    1. 判斷對象是否需要轉(zhuǎn)換成沙箱內(nèi)/外,不需要則返回此對象,需要就轉(zhuǎn)2;
    2. 創(chuàng)建沙箱內(nèi)/外對應(yīng)的對象實(shí)例;
    3. 遍歷該對象實(shí)例的每一個(gè)字段,對該字段執(zhí)行步驟1,并將復(fù)制后的值賦值給新對象中對應(yīng)字段;

    嗯,就是這樣。
    前面我們有提到,我們假定傳參對象都是值對象,所以這里的設(shè)計(jì)相對簡單,如有哪位同學(xué)需要傳非值對象,那么就需要對外部對象做代理

  4. 將代理對象返回;

有些同學(xué)關(guān)心類如何從沙箱外復(fù)制到沙箱內(nèi)重定義的是吧?這是SandboxClassLoader的核心部分,展示下代碼邏輯

class SandboxClassLoader extends ClassLoader {
    //......
    //緩存已經(jīng)創(chuàng)建過的Class實(shí)例,避免重復(fù)定義
    private final Map<String, Class> cache = Maps.newHashMap();

    /**
        * 內(nèi)部方法:獲取需要在沙箱內(nèi)運(yùn)行的Class實(shí)例
        * @param name 類名稱
        * @return 沙箱內(nèi)的類實(shí)例
        * @throws ClassNotFoundException
        */
    private synchronized Class<?> getSandboxClass(String name) throws ClassNotFoundException {
        //1、先從緩存中查找是否已經(jīng)轉(zhuǎn)載過該類,有則直接返回
        if(cache.containsKey(name)){
            return cache.get(name);
        }
        //2、緩存不存在該類時(shí),從currentContextClassLoader中復(fù)制一份到當(dāng)前緩存中
        Class<?> clz = copyClass(name);
        cache.put(name, clz);
        return clz;
    }

    /**
        * 從currentContextClassLoader中復(fù)制一份類到本ClassLoader中
        *
        * 此復(fù)制是將字節(jié)碼copy到當(dāng)前ClassLoader進(jìn)行定義,因此與sandbox外部的Class已經(jīng)完全不同實(shí)例,不能給外部直接賦值
        * @param name 待復(fù)制的類名稱
        * @return 工作在當(dāng)前ClassLoader中的Class
        * @throws ClassNotFoundException
        */
    private synchronized Class<?> copyClass(String name) throws ClassNotFoundException {
        //取得.class文件所在路徑
        String path = name.replace('.', '/') + ".class";
        //通過上下文類裝載器獲取資源句柄
        try (InputStream stream = contextClassLoader.getResourceAsStream(path)) {
            if(stream == null) throw new ClassNotFoundException(String.format("找不到類%s", name));

            //讀取所有字節(jié)內(nèi)容
            byte[] content = readFromStream(stream);
            return defineClass(name, content, 0, content.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("找不到指定的類", e);
        }
    }

    //......
}

涉及到的方法主要有兩個(gè),getSandboxClass方法主要負(fù)責(zé)獲取對象時(shí)進(jìn)行緩存層面的校驗(yàn),緩存的目的一個(gè)是加速獲取類定義的性能,一個(gè)是避免同一個(gè)類定義重復(fù)多次執(zhí)行導(dǎo)致出錯(cuò)。
copyClass顧名思義就是復(fù)制類定義,是從contextClassLoader中將類對應(yīng)的.class文件進(jìn)行復(fù)制,并在SandboxClassLoader中defineClass的過程,具體請閱讀代碼。

Sandbox中我們還有一個(gè)getEnumValue方法,過程有些類似就不重復(fù)介紹,請下載代碼閱讀。

至此,我們完成了代碼的編寫了。
至此,我們完成了新世界的構(gòu)建了!
至此,我們完成了所有工作了?。???
高興得太早了。。。
來自平行世界的救贖


到來的救贖

測試是代碼質(zhì)量的保障,是設(shè)計(jì)的保障,是運(yùn)行的保障,是......的保障,總之,就是保障。
所以,我們還要通過測試,為我們的“世界”進(jìn)行驗(yàn)證,看看它是否跟我們預(yù)期一致。
這只需要使用單元測試就可以做到了。代碼

public class SandboxTest {

    @Test
    public void getEnumValue() throws SandboxCannotCreateObjectException {
        //設(shè)定重定義的包
        Sandbox sandbox = new Sandbox("com.google.common.collect");

        //獲取沙箱內(nèi)對象,雖然是同名同值,但由于分屬沙箱內(nèi)外,因此預(yù)期應(yīng)該不等
        Enum type = sandbox.getEnumValue(com.google.common.collect.BoundType.CLOSED);
        assertNotEquals(type, com.google.common.collect.BoundType.CLOSED);

        //通過沙箱獲取非設(shè)定需要重定義的包內(nèi)對象,預(yù)期應(yīng)該是相等
        Enum property = sandbox.getEnumValue(com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH);
        assertEquals(property, com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH);
    }

    @Test
    public void createObject() throws SandboxCannotCreateObjectException, ClassNotFoundException {
        //設(shè)定重定義的包
        Sandbox sandbox = new Sandbox("com.google.common.eventbus");

        //獲取沙箱內(nèi)對象,預(yù)期中類定義應(yīng)該與沙箱外的類定義不等
        com.google.common.eventbus.EventBus bus = sandbox.createObject(com.google.common.eventbus.EventBus.class);
        assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);

        //通過名稱獲取,如上
        bus = sandbox.createObject("com.google.common.eventbus.EventBus");
        assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);

        //通過沙箱獲取無需重定義的類,預(yù)期應(yīng)該跟沙箱外相等
        List<String> list = sandbox.createObject(ArrayList.class);
        assertEquals(list.getClass(), ArrayList.class);
    }
}

運(yùn)行結(jié)果
來自平行世界的救贖
OK,測試通過~~~
來自平行世界的救贖


世界的坐標(biāo)

  • -> github
  • -> 碼云gitee

落地案例:如何在同一個(gè)Java進(jìn)程中連接多個(gè)RocketMQ服務(wù)器

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI