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