您好,登錄后才能下訂單哦!
為什么要說是救贖呢?先跟各位討論個“死亡問題”,如果你的女票或者你老婆問你,“我跟你媽落水了,你先救誰?”
哈哈,沒錯,就是這個來之中國的古老聲音,這個拷問你內心的世紀難題!怕了沒?
可以拋硬幣,也可以找個漁網一次性撈起來,可是等等,在這緊急關頭你真的有這么多時間?
此時的你肯定最想變成超人,或者修得絕世秘法“分身術”,這樣就不用做這道艱難的選擇題了。
平行宇宙論告訴我們,這世界有無數個copy,你也有無數個copy,只要找另外一個世界借一個你過來,你的內心就能得到神圣的救贖了。
好了,假設真的由一個平行世界,為了保證這個方法落地的可行性,我們還需要保證
我代表著不僅僅是名字外貌,還有我的人生種種組成才是完整的我,最佳結果是什么?平行世界的我就是從這一刻跟我分離出來的,是我的真正copy,通一個版本的copy!
這是我們討論這個問題的本質,解決不了的話,再來100個平行世界的我又能怎么樣?所以他要能干涉到我們這個世界,能在這個世界行動!可是既然是平行世界,那么肯定是無法過來的啊,怎么辦呢?大家都知道,作為高緯度的神可以投影到低緯度的世界中,通過“投影”來行動,或許我們可以這么干?
整理下思路
兩個同樣的我同時救了兩個生命中最重要的人,不踩坑不扎心,再來幾個老婆都救得了,簡直完美!
解決哲學問題而內心得到升華的我們,此時回歸真我(現實),利用僅存的圣者模式思考這個方法的現實意義。
“高大上”的工程師職業過程中,我們會遇上前人有意或者無意在代碼中留下的坑,譬如
但我們需要為這類對象創建全新一個實例去拯救世界時,除了內心被千萬草泥馬踐踏而過之外,似乎只能感受到這世界滿滿的惡意了。
不,肯定不是!
我們可是在圣者模式!!
在操蛋的現實社會中我們只是屌絲,但在0和1的世界里,我們可是神!
無所不能的神!
神愛世人,怎么會讓自己的羔羊生活在水深火熱中呢?
就像拯救你媽你老婆和你內心那樣,我們可以創造出一個平行世界出來啊,從虛無中造物不就是我們的本能么?
前面我們已經討論過世紀難題的解決方案,也給出了設計圖,此時的我們只要把這個思維轉換為由0和1組成的另一個世界的方式表達,似乎就可以了?
我們要通過救世主對象去操作一堆“待拯救的對象”,嗯,這就是救世主應該做的。
但是,另外一邊出現災難了,又有一堆“待拯救的對象”排排坐,等著救世主來拯救。
救世主說,臥槽,我TM分身乏術啊,上帝沒給我分身這個超能力,我也很無助啊。
好了,這個時候就是英雄閃亮登場的機會啦。
你爹媽不給你分身術,咱不分身啦,咱直接開一個新的世界,拉一個過來唄,別問為啥,就是這么任性。
嗯,具體操作就像如何把大象放進冰箱一樣分3步
1、新開辟一個世界;
2、復制一個救世主過去;
3、把救世主投影過來;
步驟有啦,分析下怎么執行。
我們是務實的工程師,不能吹逼,所以不應該叫新開辟世界,應該叫做制作一個相對比較隔離的環境出來,要求呢?這個環境應該
- 工作在里面的對象跟外面的能力應該是完全一樣的
- 環境外面應該是無法感知里面的情況的
- 環境內外的對象應該是完全不同的
我們暫且為這個環境命名為“沙箱”(Sandbox)吧。
以單例設計為參考,單例設計一般是寄托于類(Class)存在的,為了復制這個對象,我們需要做的是將整個Class復制一份。
我們知道Java中的Class是由ClassLoader裝載進內存的,而ClassLoader采用的是雙親委派機制,一個ClassLoader內獨有的業務對象對其它ClassLoader是不存在的,這不就完美滿足我們上面說的三個點嗎?Good,就它了!
方案:采用ClassLoader作為沙箱環境隔離
復制一個救世主過去
前面我們確定了ClassLoader方案后思路自然豁然開朗,現在考慮將Class復制進沙箱(ClassLoader)內就非常簡單啦!
我們知道,ClassLoader裝載Class時候其實是讀取.class文件,再通過ClassLoader的defineClass來實際定義一個類的,嗯,那我們將沙箱外的類定義復制過來也可以這樣,兩步
首先讀取.class內容。這個文件在哪里呢?當jar包被ClassLoader裝入內存后,通過getResource就可以將文件數據讀取到啦,完美!
在沙箱內定義類。簡單,就一個defineClass,打完收工~
嘿,別急,小心類重新定義哦,記得記錄下定義過哪些類。
對,這也是個問題。
剛剛我們有說過,不同ClassLoader的獨有業務對象對其它ClassLoader而言是不存在的!這就引發出問題了,外面無法使用里面創造出來的對象實例!
舉個例子BizObject biz = new BizObject(); //OK BizObject biz2 = Sandbox.createObject(BizObject.class); //出錯
為什么出錯呢?因為沙箱內外的BizObject是不一樣的啊,正反粒子在一起會湮滅的。。。
所以我們需要投影。
好吧,不是投影,我們需要有一個代理,在沙箱外培養一個傀儡,哦不是,是代理,對這個代理的所有操作都能反饋到沙箱內去執行。
嗯,到這里為止,我們基本將問題梳理一遍了,那么下一步。。。。。。
通過上面分析和梳理,我們基本已經確定了方向和邏輯,現在呢,萬事俱備,只缺一道神奇的東風我們就可以進入全新世界里了,那我們開始擼代碼!
等等這位同學,我們是不是漏了什么?
擼代碼前我們先要進行設計啊!
好吧,我們討論下本次需求。。。
首先,我們假定了已經設定了一個神奇的“沙箱”,沙箱內外隔離,所以內外的通信只能通過一座也是非常神奇的橋梁來進行,這就是“代理”;
當外部的某位同學需要創建一個對象但又受到各種限制的時候,他可以在沙箱內創建一個此對象的分身,然后通過分身的代理進行操作就可以實現對分身的操縱,從而達成目的。
嗯,需求只有這么多,接下來我們談談設計。
上面討論中我們決定了使用ClassLoader對沙箱內外進行隔離,可是不是直接暴露ClassLoader接口給外部使用呢?
ClassLoader能對底層類進行操作,雖然功能強大,但操作復雜度高,一不留神容易出現問題,所以我們應該對它進行封裝,僅提供我們期望用戶去使用的接口,而且我們認為它應該具備這些特點
這對ClassLoader來說有些強人所難,所以我們需要把它隱藏起來,創造一個沙箱對外提供服務,而將ClassLoader隱藏在沙箱內部,假定它叫“SandboxClassLoader”。
這樣我們就有了
四個對象了。
還有一點,上面說過我們的調用者通過代理對沙箱內對象進行操作,還記得為什么要使用代理嗎?使用代理的本質原因是沙箱內外的類分屬不同ClassLoader,即使同名類也是不同的!
同樣道理,當我們通過代理對象進行調用時,參數傳遞使用的是沙箱外的對象,進入沙箱內也是不能直接使用的,因此,我們同樣需要對這類對象進行轉換。
此處我們僅考慮值對象參數,各位同學如果關心其它對象傳參的話,需要進行類似的代理轉換,但值對象的話,我們只要進行值復制就行了,無需太過復雜處理
我們通過一幅圖來說明下這個關系
圖片很直觀,就不再重復解說啦
嗯,基本梳理應該已經非常清晰了,圖中只有藍色的“沙箱內某對象”屬于工作在沙箱內,動態創建出來的,其它都是在沙箱外;
而方框畫出了沙箱組件邊界,調用者和APPClassLoader都屬于已存在的實例無需關心,組件內部就屬于需要實現的部分了。
列一下關鍵幾個類
可以看出,Sandbox的API已經變得非常單一和簡單了。
為了簡化設計,這里規定了待創建的對象必須有無參構造函數,如果同學有需要通過有參構造函數構造對象的話,可以進行擴展實現,歡迎一起做好這個沙箱工具
為什么這里要分開枚舉和非枚舉對象呢?有同學清楚嗎?
枚舉的概念是指能有限列舉出來的東西,在java中,枚舉對象繼承自Enum,不能通過new方法進行構造,只能從枚舉的值中選取
而對象繼承自Object,大家都非常的熟悉
終于進入最重要的擼代碼環節了。。。
挑重點的代碼出來,咱擼一擼
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());
}
/**
* 沙箱對象構造方法
* @param redefinedPackages 需工作在沙箱內的包
* 此包下面所有類都在工作在沙箱內
*/
public Sandbox(String... redefinedPackages){
this(Lists.newArrayList(redefinedPackages));
}
// ......
}
先說說構造方法。
既然是沙箱對象,為什么要設計有參構造方法呢?
實際使用中,我們會考慮某些類之間內聚,當一個類放在沙箱內運行時,其它也建議放在沙箱內跑,而我們學過“單一性原則”,知道一個包內一般都是比較內聚的,所以這里設計就是指定某些package路徑,沙箱將會對這些包內對象進行接管。
對于不在這些包內的類,如果我們調用了沙箱來構造會怎么樣呢?所謂“Talk is cheap, show me the code”~~
請稍后,我們繼續構造函數,哈哈~~這個問題我們標記為問題1稍后討論
這里出現了SandboxClassLoader,使用了getContextClassLoader()
作為參數傳遞,此處做了什么呢?我們先看看SandboxClassLoader的構造方法
/**
* 沙箱隔離核心
*
* 通過ClassLoader將進行類級別的運行時隔離
*
* 此類本質上是代理了currentContextClassLoader對象,并增加了對部分需要在沙箱內運行的類處理能力
*/
class SandboxClassLoader extends ClassLoader{
//當前上下文的ClassLoader,用于尋找類實例并克隆進沙箱
private final ClassLoader contextClassLoader;
//緩存已經創建過的Class實例,避免重復定義
private final Map<String, Class> cache = Maps.newHashMap();
SandboxClassLoader(ClassLoader contextClassLoader) {
this.contextClassLoader = contextClassLoader;
}
//......
}
SandboxClassLoader的構造方法僅僅是將傳入的contextClassLoader
進行暫存備用,那么我們還是看看getContextClassLoader
方法
/**
* 獲取當前上下文的類裝載器
*
* 此類裝載器需包含MQClient相關類定義
* PS:單獨定義為一個方法,是擔心當這個上下文類裝載器滿足不了要求時可以快速更換
* @return 當前類裝載器
*/
private ClassLoader getContextClassLoader() {
//從類裝載器機制而言,線程上下文的類轉載器是最符合要求的
return Thread.currentThread().getContextClassLoader();
}
好簡單!!
其實這里是有一些設計依據的:我們要去創建一個對象,那么這個對象的類定義必然在當前代碼可訪問的。
基于這個考慮,我們可以確定,當用戶使用類似A a = Sandbox.createObject(A.class)
進行創建沙箱內對象時,A類在這段代碼執行的上下文必然可以訪問,此時我們可以通過此上下文的ClassLoader去獲取到這個A類對應的.class資源文件,然后重定義該類了。
繼續看看相關代碼,為了閱讀方便,我重新組織了下代碼結構
public class Sandbox {
private SandboxClassLoader classLoader;
//......
/**
* 在沙箱內創建指定名稱的類實例
*
* 如該名稱類不屬于redefinedPackages所指定的包內,則直接返回外部類實例
* @param clzName 待創建實例的類名稱
* @return 指定類名稱的實例對象
*/
public <T extends Object> T createObject(String clzName) throws ClassNotFoundException, SandboxCannotCreateObjectException {
Class clz = Class.forName(clzName);
return (T) createObject(clz);
}
/**
* 在沙箱內創建指定Class的實例
* @param clz 待創建實例的Class
* @return 跟clz功能相同并工作在沙箱內的類實例
*/
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的類裝載器一致,說明不是需要工作在沙箱內的對象,直接返回即可,無需代理
if(objectInSandbox.getClass().getClassLoader() == clz.getClassLoader()){
return (T) objectInSandbox;
}
/*
創建生產者的代理:由于沙箱內外的對象本質上屬于不同的類,因此需要將兩者能力橋接起來
這里采用了代理模式,通過創建沙箱外的對象實例,并將其所有方法調用通過代理轉發到沙箱內執行
另外,由于沙箱內外的所有實例都屬于不同的類,因此,對于參數和返回值還需要進行對象轉換,將沙箱內外的對象進行對等克隆
*/
//通過cglib創建對象的子類代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clz);
enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> {
Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
//調用前需對參數進行克隆,轉換為沙箱內對象
Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
Object result = targetMethod.invoke(objectInSandbox, targetArgs);
//調用后續對結果進行克隆,轉換為沙箱外對象
return util.cloneTo(result, getContextClassLoader());
});
return (T) enhancer.create();
}catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
throw new SandboxCannotCreateObjectException("無法在沙箱內創建對象", e);
}
}
//......
}
Sandbox中創建對象的主要方法出現了!為了方便閱讀,我將無關代碼剔除,僅保留createObject
方法。 T createObject(String clzName)
方法無具體實現,僅進行參數clzName
的校驗,然后就轉給T createObject(Class clz)
,因此主要分析這個方法。
其實代碼量不多(僅19行還包括各種花括號),主要都是注釋,脈絡如下
clz
在沙箱內的對于類定義clzInSandbox
,并通過clzInSandbox
的newInstance
創建該類的一個具體實例objectInSandbox
;因此這里要求clz
有無參構造函數 判斷clzInSandbox
是否運行在沙箱內,如果不是運行在沙箱內的話,無需創建代理直接將對象objectInSandbox
返回;
為什么要做這個判斷嗯?這里可以順帶解答前面的問題1了,從代碼
//如果對象的類裝載器和clz的類裝載器一致,說明不是需要工作在沙箱內的對象,直接返回即可,無需代理 if(objectInSandbox.getClass().getClassLoader() == > clz.getClassLoader()){ return (T) objectInSandbox; }
我們可以看出來,當創建出來的objectInSandbox
也是運行在外部的ClassLoader時,其實是不去創建代理的,因為它就是一個沙箱外的對象,又何必去創建代理這么多此一舉呢?
可我們明明調用的是classLoader.loadClass(clz.getName())
去取得沙箱內的類定義,為什么得到的卻是沙箱外的呢?這跟我們對SandboxClassLoader這個類的設計是否矛盾了呢?
好,去看看對應的代碼,show me the code
class SandboxClassLoader extends ClassLoader{ //當前上下文的ClassLoader,用于尋找類實例并克隆進沙箱 private final ClassLoader contextClassLoader; //...... /** * 覆蓋父類的轉載類進內存的方法 * @param name 指定類名稱 * @return 已轉載進內存的Class實例 * @throws ClassNotFoundException */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return findClass(name); } /** * 重定義類轉載邏輯 * * 1、對于需要運行在沙箱內的類(redefinedPackages中聲明),通過復制contextClassLoader類定義的方式,直接運行在此ClassLoader下 * 2、對于不需要運行在沙箱內的類,直接返回上下文類定義,以減少資源占用 * @param name 類名稱(全路徑) * @return 類定義 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if(isRedefinedClass(name)) { return getSandboxClass(name); } else { return contextClassLoader.loadClass(name); } } //...... }
看得出實際實現邏輯的代碼是findClass
方法,僅幾句而已,翻譯過來就是“需要重定義的類我們從沙箱內取得,不需要的直接從外部取”,所以會有對象的ClassLoader是外部的。
那什么是“需要重定義的類”呢?
/** * 是否需要運行在沙箱內的類 * @param name 類名稱 */ boolean isRedefinedClass(String name) { //校驗是否沙箱約定的需要重定義的包 for (String redefinedPackage : redefinedPackages) { if(name.startsWith(redefinedPackage)){ return true; } } return false; }
只要是Sandbox類構造時指定的包下面的類,統統都屬于需要重新在SandboxClassLoader中重定義的。
利用cglib庫創建objectInSandbox
的代理對象,攔截該代理對象的所有方法執行,全部轉去實際的對象objectInSandbox
中執行;
cglib創建對象的代碼不分析了,本質就是通過創建一個指定類的子類對方法進行攔截的過程;
我們關心的應該是攔截器干了什么?
enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> { Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes()); //調用前需對參數進行克隆,轉換為沙箱內對象 Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader); Object result = targetMethod.invoke(objectInSandbox, targetArgs); //調用后續對結果進行克隆,轉換為沙箱外對象 return util.cloneTo(result, getContextClassLoader()); });
我們會從沙箱內的對象中取得同名同參的方法,然后將參數進行轉換到沙箱內,再執行沙箱內對象方法并得到結果,最后還要將結果進行轉換到沙箱外對象才返回;
邏輯非常清晰,但沙箱內外對象如何轉換呢?
這里代碼有些長且無聊就不單獨貼出來了,有興趣的同學可以上github上自行下載,大體邏輯如下
- 判斷對象是否需要轉換成沙箱內/外,不需要則返回此對象,需要就轉2;
- 創建沙箱內/外對應的對象實例;
- 遍歷該對象實例的每一個字段,對該字段執行步驟1,并將復制后的值賦值給新對象中對應字段;
嗯,就是這樣。
前面我們有提到,我們假定傳參對象都是值對象,所以這里的設計相對簡單,如有哪位同學需要傳非值對象,那么就需要對外部對象做代理
有些同學關心類如何從沙箱外復制到沙箱內重定義的是吧?這是SandboxClassLoader的核心部分,展示下代碼邏輯
class SandboxClassLoader extends ClassLoader {
//......
//緩存已經創建過的Class實例,避免重復定義
private final Map<String, Class> cache = Maps.newHashMap();
/**
* 內部方法:獲取需要在沙箱內運行的Class實例
* @param name 類名稱
* @return 沙箱內的類實例
* @throws ClassNotFoundException
*/
private synchronized Class<?> getSandboxClass(String name) throws ClassNotFoundException {
//1、先從緩存中查找是否已經轉載過該類,有則直接返回
if(cache.containsKey(name)){
return cache.get(name);
}
//2、緩存不存在該類時,從currentContextClassLoader中復制一份到當前緩存中
Class<?> clz = copyClass(name);
cache.put(name, clz);
return clz;
}
/**
* 從currentContextClassLoader中復制一份類到本ClassLoader中
*
* 此復制是將字節碼copy到當前ClassLoader進行定義,因此與sandbox外部的Class已經完全不同實例,不能給外部直接賦值
* @param name 待復制的類名稱
* @return 工作在當前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));
//讀取所有字節內容
byte[] content = readFromStream(stream);
return defineClass(name, content, 0, content.length);
} catch (IOException e) {
throw new ClassNotFoundException("找不到指定的類", e);
}
}
//......
}
涉及到的方法主要有兩個,getSandboxClass
方法主要負責獲取對象時進行緩存層面的校驗,緩存的目的一個是加速獲取類定義的性能,一個是避免同一個類定義重復多次執行導致出錯。 copyClass
顧名思義就是復制類定義,是從contextClassLoader
中將類對應的.class文件進行復制,并在SandboxClassLoader中defineClass的過程,具體請閱讀代碼。
Sandbox中我們還有一個getEnumValue
方法,過程有些類似就不重復介紹,請下載代碼閱讀。
至此,我們完成了代碼的編寫了。
至此,我們完成了新世界的構建了!
至此,我們完成了所有工作了!!??
高興得太早了。。。
測試是代碼質量的保障,是設計的保障,是運行的保障,是......的保障,總之,就是保障。
所以,我們還要通過測試,為我們的“世界”進行驗證,看看它是否跟我們預期一致。
這只需要使用單元測試就可以做到了。代碼
public class SandboxTest {
@Test
public void getEnumValue() throws SandboxCannotCreateObjectException {
//設定重定義的包
Sandbox sandbox = new Sandbox("com.google.common.collect");
//獲取沙箱內對象,雖然是同名同值,但由于分屬沙箱內外,因此預期應該不等
Enum type = sandbox.getEnumValue(com.google.common.collect.BoundType.CLOSED);
assertNotEquals(type, com.google.common.collect.BoundType.CLOSED);
//通過沙箱獲取非設定需要重定義的包內對象,預期應該是相等
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 {
//設定重定義的包
Sandbox sandbox = new Sandbox("com.google.common.eventbus");
//獲取沙箱內對象,預期中類定義應該與沙箱外的類定義不等
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);
//通過沙箱獲取無需重定義的類,預期應該跟沙箱外相等
List<String> list = sandbox.createObject(ArrayList.class);
assertEquals(list.getClass(), ArrayList.class);
}
}
運行結果
OK,測試通過~~~
落地案例:如何在同一個Java進程中連接多個RocketMQ服務器
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。