真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Javaagent技術(shù)的介紹和使用

注:本文定義-在函數(shù)執(zhí)行前后增加對應的邏輯的操作統(tǒng)稱為MOCK

創(chuàng)新互聯(lián)網(wǎng)站建設(shè)由有經(jīng)驗的網(wǎng)站設(shè)計師、開發(fā)人員和項目經(jīng)理組成的專業(yè)建站團隊,負責網(wǎng)站視覺設(shè)計、用戶體驗優(yōu)化、交互設(shè)計和前端開發(fā)等方面的工作,以確保網(wǎng)站外觀精美、網(wǎng)站制作、網(wǎng)站建設(shè)易于使用并且具有良好的響應性。

1、引子
在某天與QA同學進行溝通時,發(fā)現(xiàn)QA同學有針對某個方法調(diào)用時,有讓該方法停止一段時間的需求,我對這部分的功能實現(xiàn)非常好奇,因此決定對原理進行一些深入的了解,力爭找到一種使用者盡可能少的對原有代碼進行修改的方式,以達到對應的MOCK要求。

整體的感知程度可以分為三個級別:

硬編碼
增加配置
無需任何修改
2、思路
在對方法進行mock,暫停以及異常模擬,在不知道其原理的情況下,進行猜想,思考其具體的實現(xiàn)原理,整體來說,最簡單的實現(xiàn)模型無外乎兩種:

2.1 樸素思路
假設(shè)存在如下的函數(shù)

1 public Object targetMethod(){

2 System.out.println("運行");

3 }

若想要在函數(shù)執(zhí)行后暫停一段時間、返回特定mock值或拋出特定異常,那么可以考慮修改對應的函數(shù)內(nèi)容:

public Object targetMethod(){      //在此處加入Sleep return 或 throw邏輯    System.out.println("運行");}

或使用類似代理的方法把對應的函數(shù)進行代理:

public Object proxy(){    //執(zhí)行Sleep return 或 throw邏輯      return targetMethod();}public Object targetMethod(){    System.out.println("運行");}

2.2 略成熟思路
在樸素思路的基礎(chǔ)上,我們可以看出,實現(xiàn)類似的暫停、mock和異常功能整體實現(xiàn)方案無外乎兩種:

代理模式
深入修改內(nèi)部函數(shù)
在這兩種思路的基礎(chǔ)上,我們從代理模式開始考慮(主要是代理使用的比較多,更熟悉)

2.2.1 動態(tài)代理
說起代理,最常想到的兩個詞語就是靜態(tài)代理和動態(tài)代理,二者卻別不進行詳述,對于靜態(tài)代理模式由于需要大量硬編碼,所以完全可以不用考慮。

針對動態(tài)代理來看,開始考慮最具代表性的CGLIB進行調(diào)研。

下面的代碼為一個典型的使用CGLIB進行動態(tài)代理的樣例(代理的函數(shù)為HelloInterface.sayHelllo):

public class DynamicProxy implements InvocationHandler {    private Object target;    public DynamicProxy(Object object) {        this.target = object;    }    private void before() {        System.out.println("before");    }    private void after() {        System.out.println("after");    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        Object res = null;        before();        try {            res = method.invoke(target, args);        } catch (Throwable e) {            throw e.getCause();        }        after();        return res;    }    public static void main(String[] args) throws IOException {        try {            SayHello sayHello = new SayHello();            DynamicProxy dynamicProxy = new DynamicProxy(sayHello);            HelloInterface helloInterface = (HelloInterface) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), sayHello.getClass().getInterfaces(), dynamicProxy);            helloInterface.sayHello();        } catch (Throwable e) {            e.printStackTrace();        }    }}

從上面代碼可以看出,對于CGLIB的動態(tài)代理而言,需要在原有代碼中進行硬編碼,且需要在對象初始化的時候,使用特定的方式進行初始化。因此若使用CGLIB完成MOCK,需要對應代碼的的感知程度最高,達到了硬編碼的程度。

2.2.2 AspectJ
由于使用代理方式無法在不對代碼進行修改的情況下完成MOCK,因此我們拋棄代理方式,考慮使用修改方法內(nèi)部代碼的方式進行MOCK。

基于這種思路,將目光轉(zhuǎn)向了AspectJ。

在使用AspectJ時,需要定義方法執(zhí)行前的函數(shù)以及方法執(zhí)行后的函數(shù):

@Aspectpublic 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指定對應的切點以及對應的環(huán)繞函數(shù)

                                    

但是基于以上的實現(xiàn)方式,需要對原有項目進行一定侵入,主要包含兩部分內(nèi)容:

在META-INF路徑下增加aop.xml
引入對應的切面定義的jar包
通過aspectj可以完成在硬編碼的情況下實現(xiàn)MOCK,但是這種實現(xiàn)方式受限于Aspectj自身局限,MOCK的功能代碼在編譯期就已經(jīng)添加到對應的函數(shù)中了,最晚可在運行時完成MOCK功能代碼的添加。這種方式主要有兩個缺點:

對于運行中的java進行無法在不重啟的條件下執(zhí)行新增MOCK
MOCK功能代碼嵌入到目標函數(shù)中,無法對MOCK功能代碼進行卸載,可能帶來穩(wěn)定性風險
3、 java agent介紹
由于在上述提到的各種技術(shù)都難以很好的支持在對原有項目無任何修改下完成MOCK功能的需求,在查閱資料后,將目光放至了java agent技術(shù)。

3.1 什么是java agent?
java agent本質(zhì)上可以理解為一個插件,該插件就是一個精心提供的jar包,這個jar包通過JVMTI(JVM Tool Interface)完成加載,最終借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成對目標代碼的修改。

java agent技術(shù)的主要功能如下:

可以在加載java文件之前做攔截把字節(jié)碼做修改
可以在運行期將已經(jīng)加載的類的字節(jié)碼做變更
還有其他的一些小眾的功能獲取所有已經(jīng)被加載過的類獲取所有已經(jīng)被初始化過了的類獲取某個對象的大小將某個jar加入到bootstrapclasspath里作為高優(yōu)先級被bootstrapClassloader加載將某個jar加入到classpath里供AppClassloard去加載設(shè)置某些native方法的前綴,主要在查找native方法的時候做規(guī)則匹配
3.2 java Instrumentation API
通過java agent技術(shù)進行類的字節(jié)碼修改最主要使用的就是Java Instrumentation API。下面將介紹如何使用Java Instrumentation API進行字節(jié)碼修改。

3.2.1 實現(xiàn)agent啟動方法
Java Agent支持目標JVM啟動時加載,也支持在目標JVM運行時加載,這兩種不同的加載模式會使用不同的入口函數(shù),如果需要在目標JVM啟動的同時加載Agent,那么可以選擇實現(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]。如果希望在目標JVM運行時加載Agent,則需要實現(xiàn)下面的方法:

[1] public static void agentmain(String agentArgs, Instrumentation inst); [2] public static void agentmain(String agentArgs);

這兩組方法的第一個參數(shù)AgentArgs是隨同 “–javaagent”一起傳入的程序參數(shù),如果這個字符串代表了多個參數(shù),就需要自己解析這些參數(shù)。inst是Instrumentation類型的對象,是JVM自動傳入的,我們可以拿這個參數(shù)進行類增強等操作。

3.2.2 指定Main-Class
Agent需要打包成一個jar包,在ManiFest屬性中指定“Premain-Class”或者“Agent-Class”,且需根據(jù)需求定義Can-Redefine-Classes和Can-Retransform-Classes:

Manifest-Version: 1.0preMain-Class: com.test.AgentClassArchiver-Version: Plexus ArchiverAgent-Class: com.test.AgentClassCan-Redefine-Classes: trueCan-Retransform-Classes: trueCreated-By: Apache Maven 3.3.9Build-Jdk: 1.8.0_112

3.2.3 agent加載
啟動時加載啟動參數(shù)增加-javaagent:[path],其中path為對應的agent的jar包路徑
運行中加載使用com.sun.tools.attach.VirtualMachine加載

try {  String jvmPid = 目標進行的pid;  logger.info("Attaching to target JVM with PID: " + jvmPid);  VirtualMachine jvm = VirtualMachine.attach(jvmPid);  jvm.loadAgent(agentFilePath);//agentFilePath為agent的路徑  jvm.detach();  logger.info("Attached to target JVM and loaded Java agent successfully");} catch (Exception e) {  throw new RuntimeException(e);}

3.2.4 Instrument
instrument是JVM提供的一個可以修改已加載類的類庫,專門為Java語言編寫的插樁服務(wù)提供支持。它需要依賴JVMTI的Attach API機制實現(xiàn)。在JDK 1.6以前,instrument只能在JVM剛啟動開始加載類時生效,而在JDK 1.6之后,instrument支持了在運行時對類定義的修改。要使用instrument的類修改功能,我們需要實現(xiàn)它提供的ClassFileTransformer接口,定義一個類文件轉(zhuǎn)換器。接口中的transform()方法會在類文件被加載時調(diào)用,而在transform方法里,我們可以利用上文中的ASM或Javassist對傳入的字節(jié)碼進行改寫或替換,生成新的字節(jié)碼數(shù)組后返回。

首先可以定義如下的類轉(zhuǎn)換器:

public class TestTransformer implements ClassFileTransformer {      //目標類名稱,  .分隔      private String targetClassName;    //目標類名稱,  /分隔    private String targetVMClassName;    private String targetMethodName;        public TestTransformer(String className,String methodName){        this.targetVMClassName = new String(className).replaceAll("\\.","\\/");        this.targetMethodName = methodName;        this.targetClassName=className;    }    //類加載時會執(zhí)行該函數(shù),其中參數(shù) classfileBuffer為類原始字節(jié)碼,返回值為目標字節(jié)碼,className為/分隔    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {          //判斷類名是否為目標類名        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)換器添加到對應的instrmentation中,對于已經(jīng)加載過的類使用retransformClasses對類進行重新加載:

public class AgentDemo {    private static String className = "hello.GreetingController";    private static String methodName = "getDomain";    public static void agentmain(String args, Instrumentation instrumentation) {        try {            List 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));    }}

從上圖的代碼可以看出,主方法實現(xiàn)了兩個,分別為agentmain和premain,其中

premain用于在啟動時,類加載前定義類的TransFormer,在類加載的時候更新對應的類的字節(jié)碼
agentmain用于在運行時進行類的字節(jié)碼的修改,步驟整體分為兩步注冊類的TransFormer調(diào)用retransformClasses函數(shù)進行類的重加載
4、java agent原理簡述
4.1 啟動時修改

Java agent技術(shù)的介紹和使用

啟動時修改主要是在jvm啟動時,執(zhí)行native函數(shù)的Agent_OnLoad方法,在方法執(zhí)行時,執(zhí)行如下步驟:

創(chuàng)建InstrumentationImpl對象
監(jiān)聽ClassFileLoadHook事件
調(diào)用InstrumentationImpl的loadClassAndCallPremain方法,在這個方法里會去調(diào)用javaagent里MANIFEST.MF里指定的Premain-Class類的premain方法
4.2 運行時修改
Java agent技術(shù)的介紹和使用

運行時修改主要是通過jvm的attach機制來請求目標jvm加載對應的agent,執(zhí)行native函數(shù)的Agent_OnAttach方法,在方法執(zhí)行時,執(zhí)行如下步驟:

創(chuàng)建InstrumentationImpl對象
監(jiān)聽ClassFileLoadHook事件
調(diào)用InstrumentationImpl的loadClassAndCallAgentmain方法,在這個方法里會去調(diào)用javaagent里MANIFEST.MF里指定的Agentmain-Class類的agentmain方法
4.3 ClassFileLoadHook和TransFormClassFile
在4.1和4.2節(jié)中,可以看出整體流程中有兩個部分是具有共性的,分別為:

ClassFileLoadHook
TranFormClassFile
ClassFileLoadHook是一個jvmti事件,該事件是instrument agent的一個核心事件,主要是在讀取字節(jié)碼文件回調(diào)時調(diào)用,內(nèi)部調(diào)用了TransFormClassFile函數(shù)。

TransFormClassFile的主要作用是調(diào)用java.lang.instrument.ClassFileTransformer的tranform方法,該方法由開發(fā)者實現(xiàn),通過instrument的addTransformer方法進行注冊。

通過以上描述可以看出在字節(jié)碼文件加載的時候,會觸發(fā)ClassFileLoadHook事件,該事件調(diào)用TransFormClassFile,通過經(jīng)由instrument的addTransformer注冊的方法完成整體的字節(jié)碼修改。

對于已加載的類,需要調(diào)用retransformClass函數(shù),然后經(jīng)由redefineClasses函數(shù),在讀取已加載的字節(jié)碼文件后,若該字節(jié)碼文件對應的類關(guān)注了ClassFileLoadHook事件,則調(diào)用ClassFileLoadHook事件。后續(xù)流程與類加載時字節(jié)碼替換一致。

4.4 何時進行運行時替換?
在類加載完畢后,對應的想要替換函數(shù)可能正在執(zhí)行,那么何時進行類字節(jié)碼的替換呢?

由于運行時類字節(jié)碼替換依賴于redefineClasses,那么可以看一下該方法的定義:

jvmtiErrorJvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {//TODO: add locking  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);  VMThread::execute(&op);  return (op.check_error());} /* end RedefineClasses */

其中整體的執(zhí)行依賴于VMThread,VMThread是一個在虛擬機創(chuàng)建時生成的單例原生線程,這個線程能派生出其他線程。同時,這個線程的主要的作用是維護一個vm操作隊列(VMOperationQueue),用于處理其他線程提交的vm operation,比如執(zhí)行GC等。

VmThread在執(zhí)行一個vm操作時,先判斷這個操作是否需要在safepoint下執(zhí)行。若需要safepoint下執(zhí)行且當前系統(tǒng)不在safepoint下,則調(diào)用SafepointSynchronize的方法驅(qū)使所有線程進入safepoint中,再執(zhí)行vm操作。執(zhí)行完后再喚醒所有線程。若此操作不需要在safepoint下,或者當前系統(tǒng)已經(jīng)在safepoint下,則可以直接執(zhí)行該操作了。所以,在safepoint的vm操作下,只有vm線程可以執(zhí)行具體的邏輯,其他線程都要進入safepoint下并被掛起,直到完成此次操作。

因此,在執(zhí)行字節(jié)碼替換的時候需要在safepoint下執(zhí)行,因此整體會觸發(fā)stop-the-world。


網(wǎng)站題目:Javaagent技術(shù)的介紹和使用
瀏覽路徑:http://weahome.cn/article/jdpsei.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部