日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java探针之修改类字节码文件

發(fā)布時間:2024/9/30 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java探针之修改类字节码文件 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

java探針利用了javaAgent + ASM字節(jié)碼注入工具實現(xiàn)了動態(tài)修改類文件的功能。像skywalking和arthas都使用到了這個技術(shù)。
具體原理為:

jdk1.5以后引入了javaAgent技術(shù),javaAgent是運行方法之前的攔截器。我們利用javaAgent和ASM字節(jié)碼技術(shù),在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時,同時將方法耗時及內(nèi)部調(diào)用情況放入處理器,處理器利用棧先進后出的特點對方法調(diào)用先后順序做處理,當(dāng)一個請求處理結(jié)束后,將耗時方法軌跡和入?yún)ap輸出到文件中,然后根據(jù)map中相應(yīng)參數(shù)或耗時方法軌跡中的關(guān)鍵代碼區(qū)分出我們要抓取的耗時業(yè)務(wù)。最后將相應(yīng)耗時軌跡文件取下來,轉(zhuǎn)化為xml格式并進行解析,通過瀏覽器將代碼分層結(jié)構(gòu)展示出來,方便耗時分析。

上篇我們介紹了JavaAgent的基本使用,下面介紹如何去動態(tài)的修改類的字節(jié)碼文件,這個才是agent實現(xiàn)更強大功能的核心所在!

Instrumentation接口

Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以獨立于應(yīng)用程序之外的代理程序,可以用來監(jiān)控和擴展JVM上運行的應(yīng)用程序,相當(dāng)于是JVM層面的AOP。

功能:
監(jiān)控和擴展JVM上的運行程序,它可以替換和修改java類的字節(jié)碼以便采集數(shù)據(jù),用于監(jiān)控,性能統(tǒng)計,覆蓋率分析,事件記錄等??梢杂迷诔绦騿訒r,也可以用于程序運行時動態(tài)attach。

比如說一個Java程序在JVM上運行,這時如果需要監(jiān)控JVM的狀態(tài),除了使用JDK自帶的jps等命令之外,就可以通過instrument來更直觀的獲取JVM的運行情況;
或者一個Java方法在JVM中執(zhí)行,如果我想獲取這個方法的執(zhí)行時間又不想改代碼,常用的做法是通過Spring的AOP來實現(xiàn),而AOP通過面向切面編程,而instrument是在JVM層面上直接改動java方法來實現(xiàn)。

public interface Instrumentation{//添加ClassFileTransformervoid addTransformer(ClassFileTransformer transformer, boolean canRetransform);//添加ClassFileTransformervoid addTransformer(ClassFileTransformer transformer);//移除ClassFileTransformerboolean removeTransformer(ClassFileTransformer transformer);//是否可以被重新定義boolean isRetransformClassesSupported();//重新定義Class文件void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;//是否可以修改Class文件boolean isModifiableClass(Class<?> theClass);//獲取所有加載的Class@SuppressWarnings("rawtypes")Class[] getAllLoadedClasses();//獲取指定類加載器已經(jīng)初始化的類@SuppressWarnings("rawtypes")Class[] getInitiatedClasses(ClassLoader loader);//獲取某個對象的大小long getObjectSize(Object objectToSize);//添加指定jar包到啟動類加載器檢索路徑void appendToBootstrapClassLoaderSearch(JarFile jarfile);//添加指定jar包到系統(tǒng)類加載檢索路徑void appendToSystemClassLoaderSearch(JarFile jarfile);//本地方法是否支持前綴boolean isNativeMethodPrefixSupported();//設(shè)置本地方法前綴,一般用于按前綴做匹配操作void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }

要是定義了操作java類的class文件方法,這里又涉及到了ClassFileTransformer接口,這個接口的作用是改變Class文件的字節(jié)碼,返回新的字節(jié)碼數(shù)組,源碼如下:

public interface ClassFileTransformer{byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }

ClassFileTransformer接口只有一個方法,就是改變指定類的Class文件,該接口沒有默認實現(xiàn),很顯然如果需要改變Class文件的內(nèi)容,需要改成什么樣需要使用者自己來實現(xiàn)。
如:

import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod;import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;public class MyTransformer implements ClassFileTransformer {final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";// 被處理的方法列表final static Map<String, List<String>> methodMap = new HashMap<>();public MyTransformer() {add("com.jun.sail.myservice.service.HelloService.say");add("com.jun.sail.myservice.service.HelloService.say2");}private void add(String methodString) {String className = methodString.substring(0, methodString.lastIndexOf("."));String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);List<String> list = methodMap.computeIfAbsent(className, k -> new ArrayList<>());list.add(methodName);}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) {className = className.replace("/", ".");if (methodMap.containsKey(className)) { // 判斷加載的class的包路徑是不是需要監(jiān)控的類CtClass ctclass = null;try {// 使用全稱,用于取得字節(jié)碼類<使用javassist>ctclass = ClassPool.getDefault().get(className);for (String methodName : methodMap.get(className)) {String outputStr = "\nSystem.out.println(\"this method [" + methodName+ "] cost:\" +(endTime - startTime) +\"ms.\");";// 得到這方法實例CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 根據(jù)原來的方法 創(chuàng)建新的方法,名字為原來的methodNameCtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);// 把舊方法名字改掉,否則會沖突String oldMethodName = methodName + "$old";ctmethod.setName(oldMethodName);// 構(gòu)建新的方法體StringBuilder bodyStr = new StringBuilder();bodyStr.append("{");bodyStr.append(prefix);bodyStr.append(oldMethodName).append("($$);\n");// 調(diào)用原有代碼,類似于method();($$)表示所有的參數(shù)bodyStr.append(postfix);bodyStr.append(outputStr);bodyStr.append("}");newMethod.setBody(bodyStr.toString());newMethod.setName(methodName);ctclass.addMethod(newMethod);}return ctclass.toBytecode();} catch (Exception e) {System.out.println("AAAAA" + e.getMessage());e.printStackTrace();}}return null;} }

然后在permain或agentmain方法中inst.addTransformer(new MyTransformer());,其他步驟同之前,不再贅述。

Instrumentation接口相當(dāng)于一個代理,當(dāng)執(zhí)行premain方法時,通過Instrumentation提供的API可以動態(tài)的添加管理JVM加載的Class文件,Instrumentation管理著ClassFileTransformer。
ClassFileTransformer接口可以動態(tài)的改變Class文件的字節(jié)碼,在加載字節(jié)碼的時候可以將字節(jié)碼進行動態(tài)修改,具體實現(xiàn)需要自定義實現(xiàn)類來實現(xiàn)ClassFileTransformer接口

Java字節(jié)碼生成框架大致有ASM、Javassist和byte buddy三種

  • ASM框架介紹及使用
    ASM是一種Java字節(jié)碼操控框架,能夠以二進制形式修改已有的類或是生成類,ASM可以直接生成二進制class文件也可以在類被加載入JVM之前動態(tài)改變類,只不過ASM在創(chuàng)建class字節(jié)碼時說底層JVM的匯編指令,需要使用者對class組織結(jié)構(gòu)和JVM匯編指令有一定的了解。由于Java 類存儲在.class文件中,這些類文件中包含有:類名稱、方法、屬性及字節(jié)碼,ASM從類文件中讀入信息后改變類行為、分析類信息或者直接創(chuàng)建新的類。

    著名的使用到ASM的案例便是lambda表達式、CGLIB動態(tài)代理類

    ASM框架核心類包含
    ClassReader:該類用來解析編譯過的class字節(jié)碼文件
    ClassWriter:該類用來重新構(gòu)建編譯后的類,比如修改類名、屬性、方法或者根據(jù)要求創(chuàng)建新的字節(jié)碼文件
    ClassAdapter:實現(xiàn)了ClassVisitor接口,將對它的方法調(diào)用委托給另一個ClassVisitor對象

  • Javassist及使用
    Javassit相比于ASM要簡單點,Javassit提供了更高級的API,當(dāng)時執(zhí)行效率上比ASM要差,因為ASM上直接操作的字節(jié)碼。功能和JDK自帶的反射功能類似,但是比反射要強大。

    Javassist核心類包括ClassPool:
    一個基于HashMap實現(xiàn)的CtClass對象容器,key上類名,value上這個類的CtClass對象
    CtClass:表示一個類,可以從ClassPool中獲取
    CtMethods:表示一個類的方法
    CtFields:表示類中的屬性

  • Byte Buddy及使用
    byte buddy是一個提供了API用于生成任意Java類工具包,可以生成和修改字節(jié)碼。

3. Instrumentation的實現(xiàn)原理

說起Instrumentation的原理,就不得不先提起JVMTI:
JVMTI官網(wǎng)文檔
JVMTI
JVMTI 是JVM Tool Interface 的縮寫,是 JVM 暴露出來給用戶擴展使用的接口集合,JVMTI 是基于事件驅(qū)動的,JVM每執(zhí)行一定的邏輯就會調(diào)用一些事件的回調(diào)接口,這些接口可以給用戶自行擴展來實現(xiàn)自己的邏輯。JVMTI是實現(xiàn) Debugger、Profiler、Monitor、Thread Analyser 和coverage analysis等工具的統(tǒng)一基礎(chǔ),在主流 Java 虛擬機中都有實現(xiàn)。

JVMTIAgent
JVMTI 是一套本地代碼接口,因此使用 JVMTI 需要我們與 C/C++ 以及 JNI 打交道。事實上,開發(fā)時一般采用建立一個 Agent 的方式來使用 JVMTI,它使用 JVMTI 函數(shù),設(shè)置一些回調(diào)函數(shù),并從 Java 虛擬機中得到當(dāng)前的運行態(tài)信息,并作出自己的判斷,最后還可能操作虛擬機的運行態(tài)。把 Agent 編譯成一個動態(tài)鏈接庫之后,我們就可以在 Java 程序啟動的時候來加載它(啟動加載模式)
主要有三個函數(shù):

  • Agent_OnLoad方法:如果agent是在啟動時加載的,那么在JVM啟動過程中會執(zhí)行這個agent里的Agent_OnLoad函數(shù)
  • Agent_OnAttach方法:如果agent不是在啟動時加載的,而是attach到目標程序上,然后通過load命令來加載agent,由ClassFileLoadHook event提供回調(diào),調(diào)用Agent_OnAttach方法
  • Agent_OnUnload方法:在agent卸載時調(diào)用

回到主題,Instrument 就是一種 JVMTIAgent,它實現(xiàn)了Agent_OnLoad和Agent_OnAttach兩個方法,也就是在使用時,Instrument既可以在啟動時加載,也可以在運行時動態(tài)加載

  • 啟動時加載就是在啟動時添加JVM參數(shù):-javaagent:XXXAgent.jar的方式
  • 運行時加載是通過JVM的attach機制來實現(xiàn),通過發(fā)送load命令來加載,這種方式明顯更加靈活,對監(jiān)控目標啟動也無限制,arthas的attach就是基于此
private void attachAgent(Configure configure) throws Exception {VirtualMachineDescriptor virtualMachineDescriptor = null;for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {String pid = descriptor.id();if (pid.equals(Integer.toString(configure.getJavaPid()))) {virtualMachineDescriptor = descriptor;}}VirtualMachine virtualMachine = null;try {if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 這種方式virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());} else {virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);}Properties targetSystemProperties = virtualMachine.getSystemProperties();String targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");String currentJavaVersion = System.getProperty("java.specification.version");if (targetJavaVersion != null && currentJavaVersion != null) {if (!targetJavaVersion.equals(currentJavaVersion)) {AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",currentJavaVersion, targetJavaVersion);AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.",targetSystemProperties.getProperty("java.home"));}}virtualMachine.loadAgent(configure.getArthasAgent(),configure.getArthasCore() + ";" + configure.toString());} finally {if (null != virtualMachine) {virtualMachine.detach();}}}

通過 VirtualMachine , 可以attach到當(dāng)前指定的jvm pid上,然后 virtualMachine.loadAgent()將編寫好的agent用于監(jiān)控目標。

總結(jié):

  • Instrumentation相當(dāng)于一個JVM級別的AOP

  • Instrumentation在JVM啟動的時候監(jiān)聽事件,如類加載事件,JVM觸發(fā)來指定的事件通過回調(diào)通知,并創(chuàng)建一個 Instrumentation接口的實例,然后找到MANIFEST.MF中配置的實現(xiàn)了premain方法的Class,然后將Instrumentation實例傳入premain方法中

  • premain方法會在main方法之前執(zhí)行,可以添加ClassFileTransfer來實現(xiàn)對Class文件字節(jié)碼的動態(tài)修改(并不會修改Class文件中的字節(jié)碼,而是修改已經(jīng)被JVM加載的字節(jié)碼)

  • 修改字節(jié)碼的技術(shù)可以使用開源的 ASM、javassist、byteBuddy等

  • https://blog.csdn.net/u010862794/article/details/87773434

    總結(jié)

    以上是生活随笔為你收集整理的java探针之修改类字节码文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。