1. 插樁的使用場景
在實際業(yè)務(wù)開發(fā)中,系統(tǒng)層面會有一些公共模塊需要進行實現(xiàn),類似于校驗、權(quán)限等等,在成熟的解決方案中會通過AOP的方式進行實現(xiàn)。
通常鏈路日志追蹤上,每個公司都會有ELK的解決方案,但是公司的業(yè)務(wù)線眾多的情況下,通常會要求業(yè)務(wù)系統(tǒng)在日志打印上會增加不同標記來進行區(qū)分,方便后續(xù)不同業(yè)務(wù)部門進行成本核算以及權(quán)限管控等等,也就是說在日志輸出上會有一定的格式要求。
另外,在實際排查問題中往往需要完成的上下文參數(shù)才能有助于問題的高效排查,因為平時在系統(tǒng)中主動的編寫日志,實際上是一種防御式編程了,那么一定是在寫代碼時就考慮了這種或者那種的業(yè)務(wù)異常情況,基本上在線上出現(xiàn)問題的概率會很小。大多數(shù)情況,出現(xiàn)線上問題一定是日常開發(fā)中沒有考慮的地方了,也只能通常arthas去分析。如果涉及到上下游服務(wù)時進行溝通的時候,往往上下游開發(fā)同學會詢問調(diào)用服務(wù)的參數(shù)以及鏈路的traceid,才能高效的排查。糟糕的是,如果系統(tǒng)中沒有提前埋入的話,只能臨時去加代碼,然后發(fā)布到預發(fā)等環(huán)境上,如果幸運的話能夠復現(xiàn)問題,也就能解決。針對這種情況,如果系統(tǒng)能夠自動打印出方法的上下文出入?yún)?shù)的話,在每一條鏈路上并且自動種入traceId的話,這樣就能在問題排查場景上更加高效,針對這塊日志標準化的能力可以抽象成公共基礎(chǔ)能力。
因此,在這樣的訴求下,如果涉及到日志標準化改造就需要一套通用的解決方案來進行,來完成日志格式的改造當然有很多的方式來進行推進,比如堆人集中改造:通過團隊組織層面,作為技術(shù)驅(qū)動的事項,有每個同學在原先的log.info
(其他日志級別的日志一樣)中按照公司的日志格式要求添加部門特殊的業(yè)務(wù)標記KV對。或者實現(xiàn)一套spring AOP的方案,定義一些注解提供給各個業(yè)務(wù)系統(tǒng)使用,但是針對存量代碼來說,需要投入人力去改造,在類或者方法上添加相應(yīng)的注解,這種方式也會帶來人效很低的問題。
針對上述這些問題,可以通過agent的方式來實現(xiàn)方法級別的字節(jié)碼插樁并且進行日志標準化。AOP是一類解決方案的“指導思想”,具體的落地實現(xiàn)方式會有很多,比如aspectJ,cglib等等工具,通過記錄方案的執(zhí)行耗時以及異常和方法出入?yún)硗瓿蓸I(yè)務(wù)鏈路的非侵入監(jiān)控。整體思路是,agent機制提供了“字節(jié)碼更改”的時機,字節(jié)碼插樁則是AOP的一種具體落地方式。
在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包,該包提供了一些工具幫助開發(fā)人員在 Java 程序運行時,動態(tài)修改系統(tǒng)中的 Class 類型。其中,使用該軟件包的一個關(guān)鍵組件就是 Java agent。從名字上看,似乎是個 Java 代理之類的,提供了一個可以更改class字節(jié)碼的時機。有很多開發(fā)工具都是基于Java Agent實現(xiàn)的,例如常見的熱部署JRebel,各種線上診斷工具(btrace, greys),還有阿里最近開源的arthas。
2. agent使用
2.1 agent靜態(tài)加載
Javaagent是java命令的一個參數(shù)。參數(shù) javaagent 可以用于指定一個 jar 包,并且對該 java 包有2個要求:
- 這個 jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項。
- Premain-Class 指定的那個類必須實現(xiàn) premain() 方法。
premain 方法,從字面上理解,就是運行在 main 函數(shù)之前的的類。當Java 虛擬機啟動時,在執(zhí)行 main 函數(shù)之前,JVM 會先運行-javaagent
所指定 jar 包內(nèi) Premain-Class 這個類的 premain 方法 。premain方法簽名如下:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
默認會優(yōu)先使用帶有Instrumentation的premain加載,如果加載了第一個方法,那么第二個方法就不會再去加載。如果第一個方法沒有,才會去加載第二個方法。
agent靜態(tài)啟動方式
使用 javaagent 需要幾個步驟:
- 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
- 創(chuàng)建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
- 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
- 使用參數(shù) -javaagent: jar包路徑啟動要代理的方法。
在執(zhí)行以上步驟后,JVM 會先執(zhí)行 premain 方法,大部分類加載都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統(tǒng)類,因為很多系統(tǒng)類先于 agent 執(zhí)行,而用戶類的加載肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,既然可以攔截類的加載,那么就可以去做重寫類這樣的操作,結(jié)合第三方的字節(jié)碼編譯工具,比如ASM,javassist,cglib等等來改寫實現(xiàn)類。
2.2 靜態(tài)加載示例
-
首先創(chuàng)建一個agent類,其中包含了premian方法,并且通過實現(xiàn)ClassFileTransformer接口來完成一個自定義重寫字節(jié)碼的類。
public class PremainAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new CustomClassTransformer(), true); } static class CustomClassTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("premain load class !!!"); return classfileBuffer; } } }
-
配置MAINFEST.MF文件
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.agent.example.PremainAgent
該文件的生成也可以通過maven插件配置后自動生成,具體配置如下:
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.agent.example.PremainAgent</Premain-Class> <Agent-Class>com.agent.example.PremainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
-
配置JVM參數(shù)指定agent路徑,啟動應(yīng)用
-javaagent:path-to/agent-core-0.0.1-SNAPSHOT.jar
啟動應(yīng)用后,在類加載之前會先被agent先進行攔截,可以看示例代碼的輸出:
premain load class !!! premain load class !!! premain load class !!! premain load class !!! premain load class !!! premain load class !!!
2.3 agent動態(tài)加載
premain的方式是在應(yīng)用啟動執(zhí)行main函數(shù)之前,提供了可以對類進行修改的時機。在main函數(shù)執(zhí)行之后或者說業(yè)務(wù)應(yīng)用正常運行后,再去更改類字節(jié)碼的時機只能通過agentmain方法,具體如下:
//采用attach機制,被代理的目標程序VM有可能很早之前已經(jīng)啟動,當然其所有類已經(jīng)被加載完成,這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對應(yīng)的類可以重新轉(zhuǎn)換,從而激活重新轉(zhuǎn)換的類執(zhí)行ClassFileTransformer列表中的回調(diào)
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
具體的步驟和靜態(tài)加載的基本一致:
新建agent類,其中包含agentmain方法,并在次類中完成對應(yīng)的agent邏輯。并且,如果需要完成對字節(jié)碼的更改,同樣可以實現(xiàn)ClassFileTransformer接口,將實現(xiàn)類放置到Instrumentation;
-
完成MAINFEST.MF文件,配置Agent-Class等選項,具體如下:
Agent-Class: com.agent.example.AgentMainAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
對MAINFEST.MF文件也可以通過maven插件完成配置,在打包的時候自動生成,具體配置如下:
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>com.agent.example.AgentMainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
2.4 agent掛載
動態(tài)agent的方式實際上是指業(yè)務(wù)應(yīng)用在運行中能夠注入一個agent,借助agent完成相應(yīng)的代理邏輯。那么,怎樣才能在JVM運行的時候向其完成注入,自然而然也就涉及到了兩個JVM進程之間的通信,可以通過VirtualMachine來完成。
VirtualMachine
字面意義表示一個Java 虛擬機,也就是程序需要監(jiān)控的目標虛擬機,提供了獲取系統(tǒng)信息(比如獲取內(nèi)存dump、線程dump,類信息統(tǒng)計(比如已加載的類以及實例個數(shù)等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現(xiàn)的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(進程id),遠程連接到j(luò)vm上 。
代理類注入操作只是它眾多功能中的一個,通過loadAgent
方法向jvm注冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以 在class加載前改變class的字節(jié)碼,也可以在class加載后重新加載。在調(diào)用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。
整體流程就是通過VirtualMachine類的attach(pid)
方法,便可以attach到一個運行中的java進程上,之后便可以通過loadAgent(agentJarPath)
來將agent的jar包注入到對應(yīng)的進程,然后對應(yīng)的進程會調(diào)用agentmain方法。
2.5 動態(tài)加載示例
-
首先創(chuàng)建一個包含了agentmain方法的agent類,并新建實現(xiàn)ClassFileTransformer接口的類加載到instrument中。
public class AgentMainAgent { public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("start agentmain"); inst.addTransformer(new CusDefinedClass(), true); } static class CusDefinedClass implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("agentMain load class !!!"); return classfileBuffer; } } }
將整個agent進行打包,完成MAINFEST.MF文件配置;
-
在測試類中中通過VirtualMainche類完成對agent動態(tài)掛載到正在運行的JVM進程中
public class AgentTest { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> vms = VirtualMachine.list(); for (VirtualMachineDescriptor vm : vms) { if ("com.agent.example.AgentTest".equals(vm.displayName())) { VirtualMachine machine = VirtualMachine.attach(vm.id()); machine.loadAgent("/path-to/agent-core-0.0.1-SNAPSHOT.jar"); } System.out.println(vm.displayName()); } } }
VirtualMachine.list()
可以列出當前正在運行JVM進程,示例中通過具體的進程名判斷出當前正在執(zhí)行的JVM,然后通過VirtualMachine.attach
與目標VM建立連接后,通過loadAgent的方式將agent掛載到目標VM中。示例代碼如下:start agentmain com.agent.example.AgentTest agentMain load class !!! agentMain load class !!!
agent機制提供了在應(yīng)用執(zhí)行前或者應(yīng)用執(zhí)行后,能夠獲取class字節(jié)碼的時機,并且能夠通過更改class字節(jié)碼的方式來完成相應(yīng)的業(yè)務(wù)邏輯,比如方法級別的監(jiān)控、日志標準化等等AOP常見的業(yè)務(wù)場景,這種方式對業(yè)務(wù)應(yīng)用的侵入性是最低的,并且性能是相當可觀的。在后續(xù)文章中會總結(jié)下字節(jié)碼的使用、基于字節(jié)碼插樁完成業(yè)務(wù)監(jiān)控以及實際開發(fā)中遇到問題。
參考資料
https://www.cnblogs.com/rickiyang/p/11368932.html#3812389359