java agent技术原理及简单实现
注:本文定義-在函數(shù)執(zhí)行前后增加對(duì)應(yīng)的邏輯的操作統(tǒng)稱為MOCK
1、引子
在某天與QA同學(xué)進(jìn)行溝通時(shí),發(fā)現(xiàn)QA同學(xué)有針對(duì)某個(gè)方法調(diào)用時(shí),有讓該方法停止一段時(shí)間的需求,我對(duì)這部分的功能實(shí)現(xiàn)非常好奇,因此決定對(duì)原理進(jìn)行一些深入的了解,力爭找到一種使用者盡可能少的對(duì)原有代碼進(jìn)行修改的方式,以達(dá)到對(duì)應(yīng)的MOCK要求。
整體的感知程度可以分為三個(gè)級(jí)別:
-
硬編碼
-
增加配置
-
無需任何修改
2、思路
在對(duì)方法進(jìn)行mock,暫停以及異常模擬,在不知道其原理的情況下,進(jìn)行猜想,思考其具體的實(shí)現(xiàn)原理,整體來說,最簡單的實(shí)現(xiàn)模型無外乎兩種:
2.1 樸素思路
假設(shè)存在如下的函數(shù)
| 1 2 3 | public?Object targetMethod(){ ????System.out.println("運(yùn)行"); } |
若想要在函數(shù)執(zhí)行后暫停一段時(shí)間、返回特定mock值或拋出特定異常,那么可以考慮修改對(duì)應(yīng)的函數(shù)內(nèi)容:
public Object targetMethod(){//在此處加入Sleep return 或 throw邏輯System.out.println("運(yùn)行"); }或使用類似代理的方法把對(duì)應(yīng)的函數(shù)進(jìn)行代理:
public Object proxy(){//執(zhí)行Sleep return 或 throw邏輯return targetMethod(); } public Object targetMethod(){System.out.println("運(yùn)行"); }?
2.2 略成熟思路
在樸素思路的基礎(chǔ)上,我們可以看出,實(shí)現(xiàn)類似的暫停、mock和異常功能整體實(shí)現(xiàn)方案無外乎兩種:
-
代理模式
-
深入修改內(nèi)部函數(shù)
在這兩種思路的基礎(chǔ)上,我們從代理模式開始考慮(主要是代理使用的比較多,更熟悉)
2.2.1 動(dòng)態(tài)代理
說起代理,最常想到的兩個(gè)詞語就是靜態(tài)代理和動(dòng)態(tài)代理,二者卻別不進(jìn)行詳述,對(duì)于靜態(tài)代理模式由于需要大量硬編碼,所以完全可以不用考慮。
針對(duì)動(dòng)態(tài)代理來看,開始考慮最具代表性的CGLIB進(jìn)行調(diào)研。
下面的代碼為一個(gè)典型的使用CGLIB進(jìn)行動(dòng)態(tài)代理的樣例(代理的函數(shù)為PersonService.setPerson):
public class CGlibDynamicProxy implements MethodInterceptor {public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("執(zhí)行前...");Object object = methodProxy.invokeSuper(o, objects);System.out.println("執(zhí)行后...");return object;}static class PersonService {public PersonService() {System.out.println("PersonService構(gòu)造");}public void setPerson() {System.out.println("PersonService:setPerson");}}public static void main(String[] args) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(PersonService.class);enhancer.setCallback(new CGlibDynamicProxy());PersonService proxy= (PersonService) enhancer.create();proxy.setPerson();} }從上面代碼可以看出,對(duì)于CGLIB的動(dòng)態(tài)代理而言,需要在原有代碼中進(jìn)行硬編碼,且需要在對(duì)象初始化的時(shí)候,使用特定的方式進(jìn)行初始化。因此若使用CGLIB完成MOCK,需要對(duì)應(yīng)代碼的的感知程度最高,達(dá)到了硬編碼的程度。
2.2.2 AspectJ
由于使用代理方式無法在不對(duì)代碼進(jìn)行修改的情況下完成MOCK,因此我們拋棄代理方式,考慮使用修改方法內(nèi)部代碼的方式進(jìn)行MOCK。
基于這種思路,將目光轉(zhuǎn)向了AspectJ。
在使用AspectJ時(shí),需要定義方法執(zhí)行前的函數(shù)以及方法執(zhí)行后的函數(shù):
@Aspect public class AspectJFrame {private Object before() {System.out.println("before");return new Object();}private Object after() {System.out.println("after");return new Object();}@Around("aroundPoint()")public Object doMock(ProceedingJoinPoint joinPoint) {Object object=null;before();try {object = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}after();return object;} }?
并通過aop.xml指定對(duì)應(yīng)的切點(diǎn)以及對(duì)應(yīng)的環(huán)繞函數(shù)
<aspectj><aspects><aspect name="com.test.framework.AspectJFrame"><before method="" pointcut=""/></aspect></aspects> </aspectj>?
但是基于以上的實(shí)現(xiàn)方式,需要對(duì)原有項(xiàng)目進(jìn)行一定侵入,主要包含兩部分內(nèi)容:
-
在META-INF路徑下增加aop.xml
-
引入對(duì)應(yīng)的切面定義的jar包
通過aspectj可以完成在硬編碼的情況下實(shí)現(xiàn)MOCK,但是這種實(shí)現(xiàn)方式受限于Aspectj自身局限,MOCK的功能代碼在編譯期就已經(jīng)添加到對(duì)應(yīng)的函數(shù)中了,最晚可在運(yùn)行時(shí)完成MOCK功能代碼的添加。這種方式主要有兩個(gè)缺點(diǎn):
-
對(duì)于運(yùn)行中的java進(jìn)行無法在不重啟的條件下執(zhí)行新增MOCK
-
MOCK功能代碼嵌入到目標(biāo)函數(shù)中,無法對(duì)MOCK功能代碼進(jìn)行卸載,可能帶來穩(wěn)定性風(fēng)險(xiǎn)
3、 java agent介紹
由于在上述提到的各種技術(shù)都難以很好的支持在對(duì)原有項(xiàng)目無任何修改下完成MOCK功能的需求,在查閱資料后,將目光放至了java agent技術(shù)。
3.1 什么是java agent?
java agent本質(zhì)上可以理解為一個(gè)插件,該插件就是一個(gè)精心提供的jar包,這個(gè)jar包通過JVMTI(JVM Tool Interface)完成加載,最終借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成對(duì)目標(biāo)代碼的修改。
java agent技術(shù)的主要功能如下:
-
可以在加載java文件之前做攔截把字節(jié)碼做修改
-
可以在運(yùn)行期將已經(jīng)加載的類的字節(jié)碼做變更
-
還有其他的一些小眾的功能
-
獲取所有已經(jīng)被加載過的類
-
獲取所有已經(jīng)被初始化過了的類
-
獲取某個(gè)對(duì)象的大小
-
將某個(gè)jar加入到bootstrapclasspath里作為高優(yōu)先級(jí)被bootstrapClassloader加載
-
將某個(gè)jar加入到classpath里供AppClassloard去加載
-
設(shè)置某些native方法的前綴,主要在查找native方法的時(shí)候做規(guī)則匹配
-
3.2 java Instrumentation API
通過java agent技術(shù)進(jìn)行類的字節(jié)碼修改最主要使用的就是Java Instrumentation API。下面將介紹如何使用Java Instrumentation API進(jìn)行字節(jié)碼修改。
3.2.1 實(shí)現(xiàn)agent啟動(dòng)方法
Java Agent支持目標(biāo)JVM啟動(dòng)時(shí)加載,也支持在目標(biāo)JVM運(yùn)行時(shí)加載,這兩種不同的加載模式會(huì)使用不同的入口函數(shù),如果需要在目標(biāo)JVM啟動(dòng)的同時(shí)加載Agent,那么可以選擇實(shí)現(xiàn)下面的方法:
[1] public static void premain(String agentArgs, Instrumentation inst); [2] public static void premain(String agentArgs);JVM將首先尋找[1],如果沒有發(fā)現(xiàn)[1],再尋找[2]。如果希望在目標(biāo)JVM運(yùn)行時(shí)加載Agent,則需要實(shí)現(xiàn)下面的方法:
[1] public static void agentmain(String agentArgs, Instrumentation inst); [2] public static void agentmain(String agentArgs);這兩組方法的第一個(gè)參數(shù)AgentArgs是隨同 “–javaagent”一起傳入的程序參數(shù),如果這個(gè)字符串代表了多個(gè)參數(shù),就需要自己解析這些參數(shù)。inst是Instrumentation類型的對(duì)象,是JVM自動(dòng)傳入的,我們可以拿這個(gè)參數(shù)進(jìn)行類增強(qiáng)等操作。
3.2.2 指定Main-Class
Agent需要打包成一個(gè)jar包,在ManiFest屬性中指定“Premain-Class”或者“Agent-Class”,且需根據(jù)需求定義Can-Redefine-Classes和Can-Retransform-Classes:
Manifest-Version: 1.0 preMain-Class: com.test.AgentClass Archiver-Version: Plexus Archiver Agent-Class: com.test.AgentClass Can-Redefine-Classes: true Can-Retransform-Classes: true Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_1123.2.3 agent加載
-
啟動(dòng)時(shí)加載
-
啟動(dòng)參數(shù)增加-javaagent:[path],其中path為對(duì)應(yīng)的agent的jar包路徑
-
-
運(yùn)行中加載
-
使用com.sun.tools.attach.VirtualMachine加載
-
?
3.2.4 Instrument
instrument是JVM提供的一個(gè)可以修改已加載類的類庫,專門為Java語言編寫的插樁服務(wù)提供支持。它需要依賴JVMTI的Attach API機(jī)制實(shí)現(xiàn)。在JDK 1.6以前,instrument只能在JVM剛啟動(dòng)開始加載類時(shí)生效,而在JDK 1.6之后,instrument支持了在運(yùn)行時(shí)對(duì)類定義的修改。要使用instrument的類修改功能,我們需要實(shí)現(xiàn)它提供的ClassFileTransformer接口,定義一個(gè)類文件轉(zhuǎn)換器。接口中的transform()方法會(huì)在類文件被加載時(shí)調(diào)用,而在transform方法里,我們可以利用上文中的ASM或Javassist對(duì)傳入的字節(jié)碼進(jìn)行改寫或替換,生成新的字節(jié)碼數(shù)組后返回。
首先可以定義如下的類轉(zhuǎn)換器:
public class TestTransformer implements ClassFileTransformer {//目標(biāo)類名稱, .分隔private String targetClassName;//目標(biāo)類名稱, /分隔private String targetVMClassName;private String targetMethodName;public TestTransformer(String className,String methodName){this.targetVMClassName = new String(className).replaceAll("\\.","\\/");this.targetMethodName = methodName;this.targetClassName=className;}//類加載時(shí)會(huì)執(zhí)行該函數(shù),其中參數(shù) classfileBuffer為類原始字節(jié)碼,返回值為目標(biāo)字節(jié)碼,className為/分隔public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {//判斷類名是否為目標(biāo)類名if(!className.equals(targetVMClassName)){return classfileBuffer;}try {ClassPool classPool = ClassPool.getDefault();CtClass cls = classPool.get(this.targetClassName);CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);ctMethod.insertBefore("{ System.out.println(\"start\"); }");ctMethod.insertAfter("{ System.out.println(\"end\"); }");return cls.toBytecode();} catch (Exception e) {}return classfileBuffer;} }?
類轉(zhuǎn)換器定義完畢后,需要將定義好的類轉(zhuǎn)換器添加到對(duì)應(yīng)的instrmentation中,對(duì)于已經(jīng)加載過的類使用retransformClasses對(duì)類進(jìn)行重新加載:
public class AgentDemo {private static String className = "hello.GreetingController";private static String methodName = "getDomain";public static void agentmain(String args, Instrumentation instrumentation) {try {List<Class> needRetransFormClasses = new LinkedList<>();Class[] loadedClass = instrumentation.getAllLoadedClasses();for (int i = 0; i < loadedClass.length; i++) {if (loadedClass[i].getName().equals(className)) {needRetransFormClasses.add(loadedClass[i]);}}instrumentation.addTransformer(new TestTransformer(className, methodName));instrumentation.retransformClasses(needRetransFormClasses.toArray(new Class[0]));} catch (Exception e) {}}public static void premain(String args, Instrumentation instrumentation) {instrumentation.addTransformer(new TestTransformer(className, methodName));}}?
從上圖的代碼可以看出,主方法實(shí)現(xiàn)了兩個(gè),分別為agentmain和premain,其中
-
premain
-
用于在啟動(dòng)時(shí),類加載前定義類的TransFormer,在類加載的時(shí)候更新對(duì)應(yīng)的類的字節(jié)碼
-
-
agentmain
-
用于在運(yùn)行時(shí)進(jìn)行類的字節(jié)碼的修改,步驟整體分為兩步
-
注冊(cè)類的TransFormer
-
調(diào)用retransformClasses函數(shù)進(jìn)行類的重加載
-
-
4、java agent原理簡述
4.1 啟動(dòng)時(shí)修改
?
?
啟動(dòng)時(shí)修改主要是在jvm啟動(dòng)時(shí),執(zhí)行native函數(shù)的Agent_OnLoad方法,在方法執(zhí)行時(shí),執(zhí)行如下步驟:
-
創(chuàng)建InstrumentationImpl對(duì)象
-
監(jiān)聽ClassFileLoadHook事件
-
調(diào)用InstrumentationImpl的loadClassAndCallPremain方法,在這個(gè)方法里會(huì)去調(diào)用javaagent里MANIFEST.MF里指定的Premain-Class類的premain方法
4.2 運(yùn)行時(shí)修改
?
?
運(yùn)行時(shí)修改主要是通過jvm的attach機(jī)制來請(qǐng)求目標(biāo)jvm加載對(duì)應(yīng)的agent,執(zhí)行native函數(shù)的Agent_OnAttach方法,在方法執(zhí)行時(shí),執(zhí)行如下步驟:
-
創(chuàng)建InstrumentationImpl對(duì)象
-
監(jiān)聽ClassFileLoadHook事件
-
調(diào)用InstrumentationImpl的loadClassAndCallAgentmain方法,在這個(gè)方法里會(huì)去調(diào)用javaagent里MANIFEST.MF里指定的Agentmain-Class類的agentmain方法
4.3 ClassFileLoadHook和TransFormClassFile
在4.1和4.2節(jié)中,可以看出整體流程中有兩個(gè)部分是具有共性的,分別為:
-
ClassFileLoadHook
-
TranFormClassFile
ClassFileLoadHook是一個(gè)jvmti事件,該事件是instrument agent的一個(gè)核心事件,主要是在讀取字節(jié)碼文件回調(diào)時(shí)調(diào)用,內(nèi)部調(diào)用了TransFormClassFile函數(shù)。
TransFormClassFile的主要作用是調(diào)用java.lang.instrument.ClassFileTransformer的tranform方法,該方法由開發(fā)者實(shí)現(xiàn),通過instrument的addTransformer方法進(jìn)行注冊(cè)。
通過以上描述可以看出在字節(jié)碼文件加載的時(shí)候,會(huì)觸發(fā)ClassFileLoadHook事件,該事件調(diào)用TransFormClassFile,通過經(jīng)由instrument的addTransformer注冊(cè)的方法完成整體的字節(jié)碼修改。
對(duì)于已加載的類,需要調(diào)用retransformClass函數(shù),然后經(jīng)由redefineClasses函數(shù),在讀取已加載的字節(jié)碼文件后,若該字節(jié)碼文件對(duì)應(yīng)的類關(guān)注了ClassFileLoadHook事件,則調(diào)用ClassFileLoadHook事件。后續(xù)流程與類加載時(shí)字節(jié)碼替換一致。
4.4 何時(shí)進(jìn)行運(yùn)行時(shí)替換?
在類加載完畢后,對(duì)應(yīng)的想要替換函數(shù)可能正在執(zhí)行,那么何時(shí)進(jìn)行類字節(jié)碼的替換呢?
由于運(yùn)行時(shí)類字節(jié)碼替換依賴于redefineClasses,那么可以看一下該方法的定義:
jvmtiError JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) { //TODO: add lockingVM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);VMThread::execute(&op);return (op.check_error()); } /* end RedefineClasses */其中整體的執(zhí)行依賴于VMThread,VMThread是一個(gè)在虛擬機(jī)創(chuàng)建時(shí)生成的單例原生線程,這個(gè)線程能派生出其他線程。同時(shí),這個(gè)線程的主要的作用是維護(hù)一個(gè)vm操作隊(duì)列(VMOperationQueue),用于處理其他線程提交的vm operation,比如執(zhí)行GC等。
VmThread在執(zhí)行一個(gè)vm操作時(shí),先判斷這個(gè)操作是否需要在safepoint下執(zhí)行。若需要safepoint下執(zhí)行且當(dāng)前系統(tǒng)不在safepoint下,則調(diào)用SafepointSynchronize的方法驅(qū)使所有線程進(jìn)入safepoint中,再執(zhí)行vm操作。執(zhí)行完后再喚醒所有線程。若此操作不需要在safepoint下,或者當(dāng)前系統(tǒng)已經(jīng)在safepoint下,則可以直接執(zhí)行該操作了。所以,在safepoint的vm操作下,只有vm線程可以執(zhí)行具體的邏輯,其他線程都要進(jìn)入safepoint下并被掛起,直到完成此次操作。
因此,在執(zhí)行字節(jié)碼替換的時(shí)候需要在safepoint下執(zhí)行,因此整體會(huì)觸發(fā)stop-the-world。
總結(jié)
以上是生活随笔為你收集整理的java agent技术原理及简单实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从原理上理解MySQL的优化建议
- 下一篇: 微信实时Look-alike算法分享赏析