Java代理初学者指南
盡管Java初學(xué)者很快學(xué)會了鍵入public static void main來運行他們的應(yīng)用程序,但是即使是經(jīng)驗豐富的開發(fā)人員也常常不知道JVM對Java流程的兩個附加入口點的支持: premain和agentmain方法。 這兩種方法都允許所謂的Java代理在駐留在其自己的jar文件中的同時對現(xiàn)有Java程序做出貢獻(xiàn),即使沒有被主應(yīng)用程序顯式鏈接。 這樣做,有可能與托管它們的應(yīng)用程序完全獨立地開發(fā),發(fā)行和發(fā)布Java代理,同時仍在同一Java進(jìn)程中運行它們。
最簡單的Java代理先于實際應(yīng)用程序運行,例如執(zhí)行一些動態(tài)設(shè)置。 代理可以例如安裝特定的SecurityManager或以編程方式配置系統(tǒng)屬性。 下面的類是一個不太有用的代理,仍然可以作為良好的入門演示:在將控制權(quán)傳遞給實際應(yīng)用程序的main方法之前,該類僅將一行打印到控制臺:
<pre class= "wp-block-syntaxhighlighter-code" >package sample; public class SimpleAgent<?> { public static void premain(String argument) { System.out.println( "Hello " + argument); } }< /pre >要將此類用作Java代理,需要將其包裝在jar文件中。 除常規(guī)Java程序外,無法從文件夾加載Java代理的類。 另外,需要指定一個清單條目,該清單條目引用包含premain方法的類:
Premain-Class: sample.SimpleAgent通過此設(shè)置,現(xiàn)在可以在命令行上添加Java代理,方法是指向捆綁代理的文件系統(tǒng)位置,并可以選擇在等號后添加單個參數(shù),如下所示:
java -javaagent:/location/of/agent.jar=世界some.random.Program
現(xiàn)在在some.random.Program執(zhí)行main方法之前,將打印出Hello World ,其中第二個單詞是所提供的參數(shù)。
儀表API
如果搶占式代碼執(zhí)行是Java代理的唯一功能,那么它們當(dāng)然將沒有多大用處。 實際上,大多數(shù)Java代理僅是有用的,因為Java代理可以通過將類型為Instrumentation的第二個參數(shù)添加到代理的入口點方法來請求Java代理請求。 儀器API提供對Java代理專有的JVM提供的較低級別功能的訪問,而JVM從不提供給常規(guī)Java程序。 工具API的核心是允許在Java類加載之前或之后對其進(jìn)行修改。
任何已編譯的Java類都存儲為.class文件,該文件在首次加載時以字節(jié)數(shù)組的形式呈現(xiàn)給Java代理。 通過將一個或多個ClassFileTransformer注冊到檢測API來通知代理,該API會針對當(dāng)前JVM進(jìn)程的ClassLoader加載的任何類得到通知:
package sample; public class ClassLoadingAgent { public static void premain(String argument, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(Module module, ClassLoader loader, String name, Class<?> typeIfLoaded, ProtectionDomain domain, byte[] buffer) { System.out.println( "Class was loaded: " + name); return null; } }); } }在上面的示例中,代理通過從轉(zhuǎn)換器返回null來保持不運行狀態(tài),這使轉(zhuǎn)換過程中止,但是僅將帶有最近加載的類的名稱的消息打印到控制臺。 但是,通過轉(zhuǎn)換buffer參數(shù)提供的字節(jié)數(shù)組,代理可以在加載任何類之前更改其行為。
轉(zhuǎn)換已編譯的Java類可能聽起來很復(fù)雜。 但是幸運的是, Java虛擬機規(guī)范(JVMS)詳細(xì)說明了代表類文件的每個字節(jié)的含義。 為了修改一種方法的行為,因此將識別該方法代碼的偏移量,然后向該方法添加所謂的Java字節(jié)代碼指令,以表示所需的已更改行為。 通常,這種轉(zhuǎn)換不是手動應(yīng)用的,而是通過使用字節(jié)碼處理器(最著名的是ASM庫)將類文件拆分為組件的應(yīng)用。 這樣,就可以孤立地查看字段,方法和注釋,從而可以應(yīng)用更有針對性的轉(zhuǎn)換并節(jié)省一些記賬。
無干擾的代理
盡管ASM使類文件轉(zhuǎn)換更安全,更簡單,但它仍然依賴于庫用戶對字節(jié)碼及其特征的良好理解。 但是,其他通常基于ASM的庫允許在更高級別上表達(dá)字節(jié)碼轉(zhuǎn)換,這使得這種理解成為必然。 此類庫的一個示例是Byte Buddy ,它由本文的作者開發(fā)和維護(hù)。 Byte Buddy旨在將字節(jié)碼轉(zhuǎn)換映射到大多數(shù)Java開發(fā)人員已經(jīng)知道的概念,以使代理開發(fā)更容易上手。
為了編寫Java代理,Byte Buddy提供了AgentBuilder API,該API在ClassFileTransformer創(chuàng)建并注冊ClassFileTransformer 。 字節(jié)好友ClassFileTransformer直接注冊ClassFileTransformer ,而是允許指定ElementMatcher來首先標(biāo)識感興趣的類型。 對于每種匹配類型,然后可以指定一個或多個轉(zhuǎn)換。 然后,Byte Buddy將這些指令轉(zhuǎn)換為可以安裝到Instrumentation API中的轉(zhuǎn)換器的高性能實現(xiàn)。 例如,以下代碼在Byte Buddy的API中重新創(chuàng)建了先前的非運行轉(zhuǎn)換器:
package sample; public class ByteBuddySampleAgent { public static void premain(String argument, Instrumentation instrumentation) { new AgentBuilder.Default() . type (ElementMatchers.any()) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { System.out.println( "Class was loaded: " + name); return builder; }).installOn(instrumentation); } }應(yīng)該提到的是,與前面的示例相反,Byte Buddy將轉(zhuǎn)換所有發(fā)現(xiàn)的類型,而無需應(yīng)用更改,而后者將完全忽略那些不需要的類型,效率較低。 另外,如果沒有另外指定,默認(rèn)情況下它將忽略Java核心庫的類。 但是實質(zhì)上,可以達(dá)到相同的效果,從而可以使用上述代碼演示使用Byte Buddy的簡單代理。
使用Byte Buddy建議測量執(zhí)行時間
字節(jié)伙伴不是將類文件公開為字節(jié)數(shù)組,而是嘗試將常規(guī)Java代碼編織或鏈接到已檢測類中。 這樣,Java代理的開發(fā)人員無需直接產(chǎn)生字節(jié)碼,而可以依賴于Java編程語言及其與之已有關(guān)系的現(xiàn)有工具。 對于使用Byte Buddy編寫的Java代理,行為通常由建議類表示,在這些類中,帶注釋的方法描述了添加到現(xiàn)有方法的開頭和結(jié)尾的行為。 例如,以下建議類用作模板,該模板將方法的執(zhí)行時間打印到控制臺:
public class TimeMeasurementAdvice { @Advice.OnMethodEnter public static long enter() { return System.currentTimeMillis(); } @Advice.OnMethodExit(onThrowable = Throwable.class) public static void exit (@Advice.Enter long start, @Advice.Origin String origin) { long executionTime = System.currentTimeMillis() - start; System.out.println(origin + " took " + executionTime + " to execute" ); } }在上面的建議類中,enter方法僅記錄當(dāng)前時間戳,并返回該時間戳以使其在方法末尾可用。 如圖所示,在實際方法主體之前執(zhí)行輸入建議。 在方法結(jié)束時,將應(yīng)用退出建議,在該建議中,將從當(dāng)前時間戳中減去所記錄的值,以確定該方法的執(zhí)行時間。 然后將執(zhí)行時間打印到控制臺。
為了利用建議,需要將其應(yīng)用在先前示例中仍未運行的變壓器中。 為避免打印任何方法的運行時,我們將建議的應(yīng)用程序條件MeasureTime自定義的,保留了運行時的注釋MeasureTime ,應(yīng)用程序開發(fā)人員可以將其添加到其類中。
package sample; public class ByteBuddyTimeMeasuringAgent { public static void premain(String argument, Instrumentation instrumentation) { Advice advice = Advice.to(TimeMeasurementAdvice.class); new AgentBuilder.Default() . type (ElementMatchers.isAnnotatedBy(MeasureTime.class)) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { return builder.visit(advice.on(ElementMatchers.isMethod()); }).installOn(instrumentation); } }給定上述代理程序的應(yīng)用程序之后,如果通過MeasureTime注釋了一個類,則現(xiàn)在將所有方法執(zhí)行時間打印到控制臺。 實際上,以更結(jié)構(gòu)化的方式收集此類指標(biāo)當(dāng)然更有意義,但是在已經(jīng)完成打印輸出之后,這不再是要完成的復(fù)雜任務(wù)。
動態(tài)代理附件和類重新定義
在Java 8之前,這要歸功于JDK的tools.jar中存儲的實用程序,該實用程序可以在JDK的安裝文件夾中找到。 從Java 9開始,此jar已分解到j(luò)dk.attach模塊中,該模塊現(xiàn)在可在任何常規(guī)JDK發(fā)行版中使用。 使用包含的工具API,可以使用以下代碼將JAR文件附加到具有給定進(jìn)程ID的JVM:
VirtualMachine vm = VirtualMachine.attach(processId); try { vm.loadAgent( "/location/of/agent.jar" ); } finally { vm.detach(); }當(dāng)調(diào)用上述API時,JVM將使用給定的ID定位進(jìn)程,并在該遠(yuǎn)程虛擬機內(nèi)的專用線程中執(zhí)行agent agentmain方法。 此外,此類代理可能會要求有權(quán)在其清單中重新轉(zhuǎn)換類,以更改已加載的類的代碼:
Agentmain-Class: sample.SimpleAgent Can-Retransform-Classes: true給定這些清單條目之后,代理現(xiàn)在可以請求考慮將任何已加載的類進(jìn)行重新轉(zhuǎn)換, ClassFileTransformer可以使用附加的布爾參數(shù)來注冊先前的ClassFileTransformer ,從而指示需要在重新轉(zhuǎn)換嘗試時得到通知:
package sample; public class ClassReloadingAgent { public static void agentmain(String argument, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(Module module, ClassLoader loader, String name, Class<?> typeIfLoaded, ProtectionDomain domain, byte[] buffer) { if (typeIfLoaded == null) { System.out.println( "Class was loaded: " + name); } else { System.out.println( "Class was re-loaded: " + name); } return null; } }, true ); instrumentation.retransformClasses( instrumentation.getAllLoadedClasses()); } }為了表明已經(jīng)加載了一個類,現(xiàn)在將已加載類的實例提供給轉(zhuǎn)換器,對于之前未加載的類,該實例為null 。 在以上示例的末尾,請求儀表API獲取所有已加載的類,以提交任何此類類進(jìn)行重新轉(zhuǎn)換,從而觸發(fā)轉(zhuǎn)換器的執(zhí)行。 和以前一樣,出于演示工具API的目的,將類文件轉(zhuǎn)換器實現(xiàn)為不可操作。
當(dāng)然,Byte Buddy還通過注冊重新轉(zhuǎn)換策略在其API中涵蓋了這種轉(zhuǎn)換形式,在這種情況下,Byte Buddy還將考慮所有類別以便進(jìn)行重新轉(zhuǎn)換。 這樣做,可以調(diào)整以前的時間測量代理程序,使其在動態(tài)連接的情況下也考慮加載的類:
package sample; public class ByteBuddyTimeMeasuringRetransformingAgent { public static void agentmain(String argument, Instrumentation instrumentation) { Advice advice = Advice.to(TimeMeasurementAdvice.class); new AgentBuilder.Default() .with(AgentBuilder.RetransformationStrategy.RETRANSFORMATION) .disableClassFormatChanges() . type (ElementMatchers.isAnnotatedBy(MeasureTime.class)) .transform((DynamicType.Builder<?> builder, TypeDescription type , ClassLoader loader, JavaModule module) -> { return builder.visit(advice.on(ElementMatchers.isMethod()); }).installOn(instrumentation); } }為了最終方便,Byte Buddy還提供了一個用于附加到JVM的API,該API對JVM版本和供應(yīng)商進(jìn)行了抽象,以使附加過程盡可能地簡單。 給定一個進(jìn)程ID,Byte Buddy可以通過執(zhí)行一行代碼將代理附加到JVM:
ByteBuddyAgent.attach(processId, "/location/of/agent.jar" );此外,甚至可以將當(dāng)前正在運行的同一虛擬機進(jìn)程附加到測試代理程序時特別方便的進(jìn)程:
Instrumentation instrumentation = ByteBuddyAgent. install ();此功能可以作為其自己的工件byte-buddy-agent使用 ,由于使用Instrumentation實例可以直接(例如,從一個單元中直接調(diào)用premain或agentmain方法)成為可能,因此自己嘗試嘗試自定義代理很簡單。測試,無需任何其他設(shè)置。
翻譯自: https://www.javacodegeeks.com/2019/12/a-beginners-guide-to-java-agents.html
總結(jié)
以上是生活随笔為你收集整理的Java代理初学者指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: fc无敌版安卓版下载(fc无敌版安卓)
- 下一篇: javafx 自定义控件_JavaFX技