Sun过去的世界中的JDK 11和代理
使用JDK 11后,就sun.misc.Unsafe的第一種方法。 其中, defineClass方法已刪除。 代碼生成框架通常使用此方法在現(xiàn)有的類加載器中定義新的類。 盡管此方法易于使用,但它的存在也使JVM本質(zhì)上不安全,正如其定義類的名稱所暗示的那樣。 通過允許在任何類加載器和程序包中定義一個類,就可以通過在其中定義一個類來獲得對任何程序包的程序包范圍訪問,從而突破了原本封裝的程序包或模塊的邊界。
為了刪除sun.misc.Unsafe ,OpenJDK開始提供一種在運(yùn)行時(shí)定義類的替代方法。 從版本9開始, MethodHandles.Lookup類提供了類似于不安全版本的方法defineClass 。 但是,僅對于與查找的宿主類位于同一包中的類,才允許使用類定義。 由于模塊只能解析對某個模塊擁有或已打開的包的查找,因此無法再將類注入到不打算提供此類訪問權(quán)限的包中。
使用方法句柄查找,可以在運(yùn)行時(shí)定義類foo.Qux ,如下所示:
MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(foo.Bar.class, lookup); byte[] fooQuxClassFile = createClassFileForFooQuxClass(); privateLookup.defineClass(fooQuxClassFile);為了執(zhí)行類定義,需要MethodHandles.Lookup的實(shí)例,可以通過調(diào)用MethodHandles::lookup方法來檢索該MethodHandles::lookup 。 調(diào)用后一種方法對呼叫點(diǎn)敏感。 因此,返回的實(shí)例將代表從方法內(nèi)部調(diào)用的類和包的特權(quán)。 要在另一個包中定義一個類,然后在當(dāng)前包中定義一個類,則需要使用MethodHandles::privateLookupIn對此包中的類進(jìn)行解析。 僅當(dāng)此目標(biāo)類的程序包與原始查找類位于同一模塊中,或者此包顯式打開到查找類的模塊時(shí),才有可能。 如果不滿足這些要求,則嘗試解決私有查找將引發(fā)IllegalAccessException ,從而保護(hù)JPMS隱含的邊界。
當(dāng)然,代碼生成庫也受此限制的約束。 否則,它們可能被用來創(chuàng)建和注入惡意代碼。 而且由于方法句柄的創(chuàng)建對調(diào)用站點(diǎn)敏感,因此在不要求用戶通過提供表示其模塊特權(quán)的適當(dāng)查找實(shí)例的情況下,不要求用戶做一些其他工作的情況下就不可能合并新的類定義機(jī)制。
使用Byte Buddy時(shí),所需的更改很小。 該庫使用ClassDefinitionStrategy定義類,該類負(fù)責(zé)從其二進(jìn)制格式加載類。 在Java 11之前,可以使用Reflection或sun.misc.Unsafe使用ClassDefinitionStrategy.Default.INJECTION定義一個類。 為了支持Java 11,此策略需要由ClassDefinitionStrategy.UsingLookup.of(lookup)代替,在ClassDefinitionStrategy.UsingLookup.of(lookup)中,提供的查找必須有權(quán)訪問將駐留類的包。
將cglib代理遷移到Byte Buddy
截至目前,其他代碼生成庫尚未提供這種機(jī)制,并且不確定何時(shí)以及是否添加此類功能。 尤其是對于cglib而言,由于庫的過時(shí)以及在不再更新且不會采用修改的遺留應(yīng)用程序中的廣泛使用,過去已證明API更改存在問題。 對于希望采用Byte Buddy作為更現(xiàn)代且積極開發(fā)的替代產(chǎn)品的用戶,因此以下部分將介紹可能的遷移。
例如,我們使用一個方法為以下示例類生成代理:
public class SampleClass {public String test() { return "foo"; } }為了創(chuàng)建代理,通常將代理類作為子類,在其中所有方法都將被覆蓋以調(diào)度偵聽邏輯。 為此,作為示例,我們將一個值欄附加到原始實(shí)現(xiàn)的返回值上。
通常使用Enhancer類和MethodInterceptor一起定義cglib代理。 方法攔截器提供代理實(shí)例,代理方法及其參數(shù)。 最后,它還提供了MethodProxy的實(shí)例,該實(shí)例允許調(diào)用原始代碼。
Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SampleClass.class); enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {return proxy.invokeSuper(obj, method, args) + "bar";} }); SampleClass proxy = (SampleClass) enhancer.create(); assertEquals("foobar", proxy.test());請注意,如果在代理實(shí)例上調(diào)用了諸如hashCode , equals或toString類的任何其他方法,則上述代碼將引起問題。 前兩個方法也將由攔截器分派,因此,當(dāng)cglib嘗試返回字符串類型的返回值時(shí),將導(dǎo)致類強(qiáng)制轉(zhuǎn)換異常。 相反, toString方法可以工作,但是會返回意外的結(jié)果,因?yàn)樵紝?shí)現(xiàn)的前綴是bar作為返回值。
在Byte Buddy中,代理不是專門的概念,但可以使用庫的通用代碼生成DSL進(jìn)行定義。 對于與cglib最相似的方法,使用MethodDelegation提供最簡單的遷移路徑。 這樣的委派以用戶定義的攔截器類為目標(biāo),方法調(diào)用將調(diào)度到該類:
public class SampleClassInterceptor {public static String intercept(@SuperCall Callable<String> zuper) throws Exception {return zuper.call() + "bar";} }上面的攔截器首先通過Byte Buddy按需提供的幫助程序?qū)嵗{(diào)用原始代碼。 使用Byte Buddy的代碼生成DSL來實(shí)現(xiàn)對此攔截器的委托,如下所示:
SampleClass proxy = new ByteBuddy().subclass(SampleClass.class).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded().getDeclaredConstructor().newInstance(); assertEquals("foobar", proxy.test());除了cglib之外,Byte Buddy還需要使用ElementMatcher指定方法過濾器。 盡管在cglib中完全有可能進(jìn)行過濾,但它非常麻煩并且沒有明確要求,因此很容易被遺忘。 在Byte Buddy中,仍然可以使用ElementMatchers.any()匹配器攔截所有方法,但是通過要求指定這樣的匹配器,希望提醒用戶做出有意義的選擇。
使用上述匹配器,每當(dāng)調(diào)用名為test的方法時(shí),都會使用所討論的方法委派將調(diào)用委派給指定的攔截器。
但是,引入的攔截器將無法分派不返回字符串實(shí)例的方法。 實(shí)際上,代理的創(chuàng)建將產(chǎn)生由Byte Buddy發(fā)出的異常。 但是,完全有可能定義一個更通用的攔截器,該攔截器可應(yīng)用于與cglib的MethodInterceptor提供的方法類似的任何方法:
public class SampleClassInterceptor {@RuntimeTypepublic static Object intercept(@Origin Method method,@This Object self,@AllArguments Object[] args,@SuperCall Callable<String> zuper) throws Exception {return zuper.call() + "bar";} }當(dāng)然,由于在這種情況下不使用攔截器的其他參數(shù),因此可以省略它們,從而使代理更有效。 Byte Buddy僅在需要時(shí)才按需提供參數(shù)。
由于上述代理是無狀態(tài)的,因此將攔截方法定義為靜態(tài)。 同樣,這是一個簡單的優(yōu)化,因?yàn)锽yte Buddy否則需要在代理類中定義一個字段,該字段保存對攔截器實(shí)例的引用。 但是,如果需要實(shí)例,則可以使用MethodDelegation.to(new SampleClassInterceptor())將委托定向到實(shí)例的成員方法。
緩存代理類以提高性能
使用字節(jié)伙伴時(shí),不會自動緩存代理類。 這意味著每次運(yùn)行上述代碼時(shí),都會生成并加載一個新類。 由于代碼生成和類定義是昂貴的操作,因此這當(dāng)然效率低下,如果可以重復(fù)使用代理類,則應(yīng)避免這種情況。 在cglib中,如果兩次增強(qiáng)的輸入相同,則返回先前生成的類,這通常在兩次運(yùn)行同一代碼段時(shí)是正確的。 然而,由于通常可以更容易地計(jì)算高速緩存密鑰,因此該方法相當(dāng)容易出錯并且通常效率低下。 使用字節(jié)伙伴,可以使用專用的緩存庫(如果已有的話)。 另外,Byte Buddy還提供了TypeCache ,它通過用戶定義的緩存鍵為類實(shí)現(xiàn)了簡單的緩存。 例如,可以使用以下代碼使用基類作為鍵來緩存以上類的生成:
TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT); Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy().subclass(SampleClass.class).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded() });不幸的是,Java中的緩存類帶來了一些警告。 如果創(chuàng)建了代理,則它當(dāng)然會繼承它所代理的類的子類,從而使該基類不適合進(jìn)行垃圾收集。 因此,如果代理類被強(qiáng)引用,則密鑰也將被強(qiáng)引用。 這將使高速緩存無用,并為內(nèi)存泄漏打開。 因此,必須通過構(gòu)造函數(shù)參數(shù)指定的內(nèi)容來輕而易舉地引用代理類。 將來,如果Java引入了星歷作為參考類型,則可能會解決此問題。 同時(shí),如果不存在代理類垃圾回收的問題,則可以使用ConcurrentMap在不存在時(shí)計(jì)算值。
擴(kuò)展代理類的可用性
為了使用代理類的重用,將代理類重構(gòu)為無狀態(tài)并將狀態(tài)隔離到實(shí)例字段中通常是有意義的。 然后可以在偵聽期間使用上述依賴項(xiàng)注入機(jī)制來訪問此字段,例如,以使后綴值可針對每個代理實(shí)例進(jìn)行配置:
public class SampleClassInterceptor {public static String intercept(@SuperCall Callable<String> zuper, @FieldValue("qux") String suffix) throws Exception {return zuper.call() + suffix;} }上面的攔截器現(xiàn)在接收字段qux的值作為第二個參數(shù),可以使用Byte Buddy的類型創(chuàng)建DSL聲明它:
TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT); Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy().subclass(SampleClass.class).defineField(“qux”, String.class, Visibility.PUBLIC).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded() });現(xiàn)在,可以使用Java反射在每個實(shí)例創(chuàng)建后在每個實(shí)例上設(shè)置該字段值。 為了避免反射,DSL還可以用于實(shí)現(xiàn)一些接口,該接口聲明用于所提及字段的設(shè)置方法,可以使用Byte Buddy的FieldAccessor實(shí)現(xiàn)來實(shí)現(xiàn)。
加權(quán)代理運(yùn)行時(shí)和創(chuàng)建性能
最后,在使用Byte Buddy創(chuàng)建代理時(shí),需要考慮一些性能。 在生成代碼時(shí),需要在代碼生成本身的性能與所生成代碼的運(yùn)行時(shí)性能之間進(jìn)行權(quán)衡。 與cglib或其他proxing庫相比,Byte Buddy通常旨在創(chuàng)建盡可能高效地運(yùn)行的代碼,這可能需要更多時(shí)間來創(chuàng)建此類代碼。 這是基于這樣的假設(shè),即大多數(shù)應(yīng)用程序運(yùn)行時(shí)間很長,但是一次只能創(chuàng)建代理,但是代理不適用于所有類型的應(yīng)用程序。
與cglib的一個重要區(qū)別是,Byte Buddy為每個方法生成一個專用的超級調(diào)用委托,該方法被攔截,而不是單個MethodProxy 。 這些附加的類需要花費(fèi)更多的時(shí)間來創(chuàng)建和加載,但是使這些類可用可以為每個方法執(zhí)行帶來更好的運(yùn)行時(shí)性能。 如果在循環(huán)中調(diào)用代理方法,則這種差異很快就很關(guān)鍵。 但是,如果運(yùn)行時(shí)性能不是主要目標(biāo),并且在短時(shí)間內(nèi)創(chuàng)建代理類更重要,則以下方法可避免完全創(chuàng)建其他類:
public class SampleClassInterceptor {public static String intercept(@SuperMethod Method zuper, @This Object target, @AllArguments Object[] arguments) throws Exception {return zuper.invoke(target, arguments) + "bar";} }模塊化環(huán)境中的代理
對攔截器使用簡單形式的依賴注入,而不是依賴于特定于庫的類型,例如cglib的
MethodInterceptor ,Byte Buddy在模塊化環(huán)境中提供了另一個優(yōu)勢:由于生成的代理類將直接引用攔截器類,而不是引用特定于庫的調(diào)度程序類型(例如cglib的MethodInterceptor ,因此被代理類的模塊不需要讀取Byte Buddy的模塊。 對于cglib,代理類模塊必須讀取cglib的模塊,該模塊定義了MethodInterceptor接口,而不是實(shí)現(xiàn)該接口的模塊。 對于使用cglib作為傳遞依賴的庫的用戶,這很可能是不直觀的,特別是如果將后者依賴視為不應(yīng)公開的實(shí)現(xiàn)細(xì)節(jié)。
在某些情況下,代理類的模塊讀取提供攔截器的框架模塊甚至是不可能或不希望的。 對于這種情況,Byte Buddy還提供了一種解決方案,通過使用它來完全避免這種依賴性
Advice組件。 該組件可用于以下示例中的代碼模板:
上面的代碼看起來似乎沒有多大意義,實(shí)際上,它將永遠(yuǎn)不會執(zhí)行。 該類僅用作Byte Buddy的字節(jié)代碼模板,后者可讀取帶注釋的方法的字節(jié)代碼,然后將其內(nèi)聯(lián)到生成的代理類中。 為此,必須對上述方法的每個參數(shù)進(jìn)行注釋,以代表代理方法的值。 在上述情況下,注釋定義了參數(shù),以定義方法的返回值,在給定模板的情況下,將bar添加為后綴。 給定此建議類,可以如下定義代理類:
new ByteBuddy().subclass(SampleClass.class).defineField(“qux”, String.class, Visibility.PUBLIC).method(ElementMatchers.named(“test”)).intercept(Advice.to(SampleClassAdvice.class).wrap(SuperMethodCall.INSTANCE)).make()通過將建議包裝在SuperMethodCall周圍,??將在對覆蓋方法的調(diào)用完成后內(nèi)聯(lián)上述建議代碼。 要在原始方法調(diào)用之前內(nèi)聯(lián)代碼,可以使用OnMethodEnter批注。
9和10之前的Java版本上的支持代理
在為JVM開發(fā)應(yīng)用程序時(shí),通常可以依靠在特定版本上運(yùn)行的應(yīng)用程序也可以在更高版本上運(yùn)行。 即使使用了內(nèi)部API,也已經(jīng)有很長時(shí)間了。 但是,由于刪除了此內(nèi)部API,從Java 11開始,這種情況不再適用,在Java 11上,依賴于sun.misc.Unsafe代碼生成庫將不再起作用。 同時(shí),通過MethodHandles.Lookup類定義MethodHandles.Lookup用于版本9之前的JVM。
對于Byte Buddy,用戶有責(zé)任使用與當(dāng)前JVM兼容的類加載策略。 為了支持所有JVM,需要進(jìn)行以下選擇:
ClassLoadingStrategy<ClassLoader> strategy; if (ClassInjector.UsingLookup.isAvailable()) {Class<?> methodHandles = Class.forName("java.lang.invoke.MethodHandles");Object lookup = methodHandles.getMethod("lookup").invoke(null);Method privateLookupIn = methodHandles.getMethod("privateLookupIn", Class.class, Class.forName("java.lang.invoke.MethodHandles$Lookup"));Object privateLookup = privateLookupIn.invoke(null, targetClass, lookup);strategy = ClassLoadingStrategy.UsingLookup.of(privateLookup); } else if (ClassInjector.UsingReflection.isAvailable()) {strategy = ClassLoadingStrateg.Default.INJECTION; } else {throw new IllegalStateException(“No code generation strategy available”); }上面的代碼使用反射來解析方法句柄查找并對其進(jìn)行解析。 這樣做,可以在Java 9之前的JDK上編譯和加載代碼。不幸的是,由于MethodHandles::lookup是調(diào)用站點(diǎn)敏感的,因此Byte Buddy無法實(shí)現(xiàn)此代碼,因此必須在駐留在其中的類中定義以上內(nèi)容用戶的模塊,而不在Byte Buddy中。
最后,值得考慮的是完全避免類注入。 代理類也可以使用ClassLoadingStrategy.Default.WRAPPER策略在自己的類加載器中定義。 該策略不使用任何內(nèi)部API,并且可以在任何JVM版本上使用。 但是,必須牢記創(chuàng)建專用類加載器的性能成本。 最后,即使代理類的軟件包名稱與代理類相同,通過在不同的類加載器中定義代理,JVM也不會將其運(yùn)行時(shí)軟件包視為等同,因此不允許覆蓋任何軟件包,私人方法。
最后的想法
最后一點(diǎn),我想表達(dá)我的觀點(diǎn),盡管遷移成本很高,但退出sun.misc.Unsafe是朝著更安全,模塊化的JVM邁出的重要一步。 在刪除此非常強(qiáng)大的類之前,可以使用sun.misc.Unsafe仍然提供的特權(quán)訪問來繞過JPMS設(shè)置的任何邊界。 如果不進(jìn)行此刪除,則JPMS會付出額外封裝帶來的所有不便,而無法依靠它。
JVM上的大多數(shù)開發(fā)人員很可能永遠(yuǎn)不會遇到這些附加限制的任何問題,但是如上所述,代碼生成和代理庫需要適應(yīng)這些更改。 對于cglib,不幸的是,這確實(shí)意味著道路的盡頭。 Cglib最初被建模為Java內(nèi)置代理API的更強(qiáng)大版本,在該版本中,它要求代理類引用其自己的調(diào)度程序API,這與Java API要求引用其類型的方式類似。 但是,這些后一種類型駐留在java.base模塊中,該模塊始終由任何模塊讀取。 因此,Java代理API仍然可以正常運(yùn)行,而cglib模型則無法修復(fù)。 過去,這已經(jīng)使cglib成為OSGi環(huán)境中的難題,但是對于JPMS,作為庫的cglib不再起作用。 Javassist提供的相應(yīng)代理API存在類似問題。
這種變化的好處是,JVM最終提供了一個穩(wěn)定的API,用于在應(yīng)用程序的運(yùn)行時(shí)定義類,這是一種依賴內(nèi)部API二十多年的常見操作。 除了我認(rèn)為仍然需要更靈活方法的Javaagents以外,這意味著在所有代理用戶完成此最終遷移之后,可以保證將來的Java版本始終能夠正常工作。 鑒于cglib的開發(fā)多年來一直處于休眠狀態(tài),并且該庫受到許多限制,因此無論如何,今天的庫用戶最終遷移都是不可避免的。 Javassist代理可能也是如此,因?yàn)楹笳邘煸诮肽陜?nèi)也沒有提交。
翻譯自: https://www.javacodegeeks.com/2018/04/jdk-11-and-proxies-in-a-world-past-sun-misc-unsafe.html
總結(jié)
以上是生活随笔為你收集整理的Sun过去的世界中的JDK 11和代理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑右下角突然跳出的小窗口笔记本电脑突然
- 下一篇: jvm上的随机数与熵_向您的JVM添加一