JVM插码之三:javaagent介绍及javassist介绍
本文介紹一下,當下比較基礎但是使用場景卻很多的一種技術,稍微偏底層點,就是字節碼插莊技術了...,如果之前大家熟悉了asm,cglib以及javassit等技術,那么下面說的就很簡單了...,因為下面要說的功能就是基于javassit實現的,接下來先從javaagent的原理說起,最后會結合一個完整的實例演示實際中如何使用。
1、什么是javassist?
Javassist是一個開源的分析、編輯和創建Java字節碼的類庫。其主要的優點,在于簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成
2、Javassist 作用?
a. 運行時監控插樁埋點
b. AOP動態代理實現(性能上比Cglib生成的要慢)
c. 獲取訪問類結構信息:如獲取參數名稱信息
3、Javassist使用流程
4、 如何對WEB項目對象進行字節碼插樁
1.統一獲取HttpRequest請求參數插樁示例
2.獲取HttpRequest參數遇到ClassNotFound的問題
3.Tomcat ClassLoader 介紹,及javaagent jar包加載機制
4.通過class 加載沉機制實現在javaagent 引用游jar 包
?
javaagent的主要功能有哪些?
?
JVMTI
JVM Tool Interface,是jvm暴露出來的一些供用戶擴展的接口集合,JVMTI是基于事件驅動的,JVM每執行到一定的邏輯就會調用一些事件的回調接口(如果有的話),這些接口可以供開發者去擴展自己的邏輯。
比如說我們最常見的想在某個類的字節碼文件讀取之后類定義之前能修改相關的字節碼,從而使創建的class對象是我們修改之后的字節碼內容,那我們就可以實現一個回調函數賦給JvmtiEnv(JVMTI的運行時,通常一個JVMTIAgent對應一個jvmtiEnv,但是也可以對應多個)的回調方法集合里的ClassFileLoadHook,這樣在接下來的類文件加載過程中都會調用到這個函數里來了,大致實現如下:
jvmtiEventCallbacks callbacks;jvmtiEnv * jvmtienv = jvmti(agent);jvmtiError jvmtierror;memset(&callbacks, 0, sizeof(callbacks));callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,&callbacks,sizeof(callbacks));JVMTIAgent
JVMTIAgent其實就是一個動態庫,利用JVMTI暴露出來的一些接口來干一些我們想做但是正常情況下又做不到的事情,不過為了和普通的動態庫進行區分,它一般會實現如下的一個或者多個函數:
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved);JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved);JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm);說到javaagent必須要講的是一個叫做instrument的JVMTIAgent(linux下對應的動態庫是libinstrument.so),因為就是它來實現javaagent的功能的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),從這名字里也完全體現了其最本質的功能:就是專門為java語言編寫的插樁服務提供支持的。
INSTRUMENT?AGENT
instrument agent實現了Agent_OnLoad和Agent_OnAttach兩方法,也就是說我們在用它的時候既支持啟動的時候來加載agent,也支持在運行期來動態來加載這個agent,其中啟動時加載agent還可以通過類似-javaagent:myagent.jar的方式來間接加載instrument agent,運行期動態加載agent依賴的是jvm的attach機制JVM Attach機制實現,通過發送load命令來加載agent。
這里解釋下幾個重要項:
- mNormalEnvironment:主要提供正常的類transform及redefine功能的。
- mRetransformEnvironment:主要提供類retransform功能的。
- mInstrumentationImpl:這個對象非常重要,也是我們java agent和JVM進行交互的入口,或許寫過javaagent的人在寫premain以及agentmain方法的時候注意到了有個Instrumentation的參數,這個參數其實就是這里的對象。
- mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在啟動的時候加載的,那該方法會被調用。
- mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,該方法在通過attach的方式動態加載agent的時候調用。
- mTransform:指向sun.instrument.InstrumentationImpl.transform方法。
- mAgentClassName:在我們javaagent的MANIFEST.MF里指定的Agent-Class。
- mOptionsString:傳給agent的一些參數。
- mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF里設置Can-Redefine-Classes:true。
- mNativeMethodPrefixAvailable:是否支持native方法前綴設置,通樣在javaagent的MANIFEST.MF里設置Can-Set-Native-Method-Prefix:true。
- mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定義了Can-Retransform-Classes:true,那將會設置mRetransformEnvironment的mIsRetransformer為true。
紅色標注的是我們最常用的,下面的列子也是會用到的...,接下來看一個具體的例子,如果熟悉分布式調用鏈系統的人肯定知道,調用鏈中最基礎的一個功能就是統計一個服務里面的某個方法執行了多長時間...,其實這個就目前來說大多數系統底層都是基于字節碼插樁技術實現的,接下來就演示一個完整的例子....,定義一個業務類,類里面定義幾個方法,然后在執行這個方法的時候,會動態實現方法的耗時統計。
看業務類定義:
package com.dxz.chama.service;import java.util.LinkedList; import java.util.List;/*** 模擬數據插入服務**/ public class InsertService {public void insert2(int num) {List<Integer> list = new LinkedList<>();for (int i = 0; i < num; i++) {list.add(i);}}public void insert1(int num) {List<Integer> list = new LinkedList<>();for (int i = 0; i < num; i++) {list.add(i);}}public void insert3(int num) {List<Integer> list = new LinkedList<>();for (int i = 0; i < num; i++) {list.add(i);}} }?
刪除服務:
package com.dxz.chama.service;import java.util.List;public class DeleteService {public void delete(List<Integer>list){for (int i=0;i<list.size();i++){list.remove(i);}} }?
ok,接下來就是要編寫javaagent的相關實現:
定義agent的入口
package com.dxz.chama.javaagent;import java.lang.instrument.Instrumentation;/*** agent的入口類*/ public class TimeMonitorAgent {// peremain 這個方法名稱是固定寫法 不能寫錯或修改public static void premain(String agentArgs, Instrumentation inst) {System.out.println("execute insert method interceptor....");System.out.println(agentArgs);// 添加自定義類轉換器inst.addTransformer(new TimeMonitorTransformer(agentArgs));} }?
接下來看最重要的Transformer的實現:
package com.dxz.chama.javaagent;import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.Objects;import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod;/*** 類方法的字節碼替換*/ public class TimeMonitorTransformer implements ClassFileTransformer {private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n";private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n";private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result";private static final String EMPTY = "";private String classNameKeyword;public TimeMonitorTransformer(String classNameKeyword){this.classNameKeyword = classNameKeyword;}/**** @param classLoader 默認類加載器* @param className 類名的關鍵字 因為還會進行模糊匹配* @param classBeingRedefined* @param protectionDomain* @param classfileBuffer* @return* @throws IllegalClassFormatException*/public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {className = className.replace("/", ".");CtClass ctClass = null;try {//使用全稱,用于取得字節碼類ctClass = ClassPool.getDefault().get(className);//匹配類的機制是基于類的關鍵字 這個是客戶端傳過來的參數 滿足就會獲取所有的方法 不滿足跳過if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){//所有方法CtMethod[] ctMethods = ctClass.getDeclaredMethods();//遍歷每一個方法for(CtMethod ctMethod:ctMethods){//修改方法的字節碼 transformMethod(ctMethod, ctClass); }}//重新返回修改后的類return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}return null;}/*** 為每一個攔截到的方法 執行一個方法的耗時操作* @param ctMethod* @param ctClass* @throws Exception*/private void transformMethod(CtMethod ctMethod,CtClass ctClass) throws Exception{//抽象的方法是不能修改的 或者方法前面加了final關鍵字if((ctMethod.getModifiers()&Modifier.ABSTRACT)>0){return;}//獲取原始方法名稱String methodName = ctMethod.getName();String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" +(endTime - startTime) +\"ms.\");";//實例化新的方法名稱String newMethodName = methodName + "$impl";//設置新的方法名稱 ctMethod.setName(newMethodName);//創建新的方法,復制原來的方法 ,名字為原來的名字CtMethod newMethod = CtNewMethod.copy(ctMethod,methodName, ctClass, null);StringBuilder bodyStr = new StringBuilder();//拼接新的方法內容bodyStr.append("{");//返回類型CtClass returnType = ctMethod.getReturnType();//是否需要返回boolean hasReturnValue = (CtClass.voidType != returnType);if (hasReturnValue) {String returnClass = returnType.getName();bodyStr.append("\n").append(returnClass + " " + METHOD_RUTURN_VALUE_VAR + ";");}bodyStr.append(START_TIME);if (hasReturnValue) {bodyStr.append("\n").append(METHOD_RUTURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);");} else {bodyStr.append("\n").append(newMethodName + "($$);");}bodyStr.append(END_TIME);bodyStr.append(monitorStr);if (hasReturnValue) {bodyStr.append("\n").append("return " + METHOD_RUTURN_VALUE_VAR+" ;");}bodyStr.append("}");//替換新方法 newMethod.setBody(bodyStr.toString());//增加新方法 ctClass.addMethod(newMethod);} }?
其實也很簡單就兩個類就實現了要實現的功能,那么如何使用呢?需要把上面的代碼打成jar包才能執行,建議大家使用maven打包,下面是pom.xml的配置文件
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.dxz</groupId><artifactId>chama</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>chama</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>javassist</groupId><artifactId>javassist</artifactId><version>3.12.1.GA</version></dependency><!-- https://mvnrepository.com/artifact/cglib/cglib --><dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.2.5</version></dependency><!-- https://mvnrepository.com/artifact/oro/oro --><dependency><groupId>oro</groupId><artifactId>oro</artifactId><version>2.0.8</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target><encoding>utf-8</encoding></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.0.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><manifestEntries><Premain-Class>com.dxz.chama.javaagent.TimeMonitorAgent</Premain-Class></manifestEntries></transformer></transformers></configuration></execution></executions></plugin></plugins></build> </project>?
強調一下,紅色標準的非常關鍵,因為如果要想jar能夠運行,必須要把運行清單打包到jar中,且一定要讓jar的主類是Permain-Class,否則無法運行,運行清單的目錄是這樣的.
mvn -clean package如果打包正確的話,里面的內容應該如下所示:
OK至此整體代碼和打包就完成了,那么接下來再講解如何使用
部署方式:
1 基于IDE開發環境運行
首先,編寫一個service的測試類如下:
package com.dxz.chama.service;import java.util.LinkedList; import java.util.List;public class ServiceTest {public static void main(String[] args) {// 插入服務InsertService insertService = new InsertService();// 刪除服務DeleteService deleteService = new DeleteService();System.out.println("....begnin insert....");insertService.insert1(1003440);insertService.insert2(2000000);insertService.insert3(30003203);System.out.println(".....end insert.....");List<Integer> list = new LinkedList<>();for (int i = 0; i < 29988440; i++) {list.add(i);}System.out.println(".....begin delete......");deleteService.delete(list);System.out.println("......end delete........");} }選擇編輯配置:如下截圖所示
?
service是指定要攔截類的關鍵字,如果這里的參數是InsertService,那么DeleteService相關的方法就無法攔截了。同理也是一樣的。
chama-0.0.1-SNAPSHOT.jar這個就是剛剛編寫那個javaagent類的代碼打成的jar包,ok 讓我們看一下最終的效果如何:
?
實際應用場景中,可以把這些結果寫入到log然后發送到es中,就可以做可視化數據分析了...還是蠻強大的,接下來對上面的業務進行擴展,因為上面默認是攔截類里面的所有方法,如果業務需求是攔截類的特定的方法該怎么實現呢?其實很簡單就是通過正則匹配,下面給出核心代碼:
定義入口agent:
package com.dxz.chama.javaagent.patter; import java.lang.instrument.Instrumentation;public class TimeMonitorPatterAgent {public static void premain(String agentArgs, Instrumentation inst) {inst.addTransformer(new PatternTransformer());} }?
定義transformer:
package com.dxz.chama.javaagent.patter;import javassist.CtClass; import org.apache.oro.text.regex.PatternCompiler; import org.apache.oro.text.regex.PatternMatcher; import org.apache.oro.text.regex.Perl5Compiler; import org.apache.oro.text.regex.Perl5Matcher;import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;public class PatternTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {PatternMatcher matcher = new Perl5Matcher();PatternCompiler compiler = new Perl5Compiler();// 指定的業務類String interceptorClass = "com.dxz.chama.service.InsertService";// 指定的方法String interceptorMethod = "insert1";try {if (matcher.matches(className, compiler.compile(interceptorClass))) {ByteCode byteCode = new ByteCode();CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod);return ctClass.toBytecode();}} catch (Exception e) {e.printStackTrace();}return null;} }?
修改字節碼的實現:
package com.dxz.chama.javaagent.patter;import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod;public class ByteCode {public CtClass modifyByteCode(String className, String method) throws Exception {ClassPool classPool = ClassPool.getDefault();CtClass ctClass = classPool.get(className);CtMethod oldMethod = ctClass.getDeclaredMethod(method);String oldMethodName = oldMethod.getName();String newName = oldMethodName + "$impl";oldMethod.setName(newName);CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null);StringBuffer sb = new StringBuffer();sb.append("{");sb.append("\nSystem.out.println(\"start to modify bytecode\");\n");sb.append("long start = System.currentTimeMillis();\n");sb.append(newName + "($$);\n");sb.append("System.out.println(\"call method" + oldMethodName + "took\"+(System.currentTimeMillis()-start))");sb.append("}");newMethod.setBody(sb.toString());ctClass.addMethod(newMethod);return ctClass;} }OK,
修改下pom中的
<manifestEntries><Premain-Class>com.dxz.chama.javaagent.patter.TimeMonitorPatterAgent</Premain-Class></manifestEntries>?
這個時候再重新打包,然后修改上面的運行配置之后再看效果,只能攔截到insert1方法
?
最后 再說一下如何使用jar運行,其實很簡單如下:把各個項目都打成jar,比如把上面的service打成service.jar,然后使用java命令運行:
java -javaagent:d://chama-0.0.1-SNAPSHOT.jar=Service -jar service.jar,效果是一樣的!
轉載于:https://www.cnblogs.com/duanxz/p/4957932.html
總結
以上是生活随笔為你收集整理的JVM插码之三:javaagent介绍及javassist介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 预算单位和非预算单位的区别
- 下一篇: 可立克为什么是5G 产品可用于5G设备上