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的主要功能有哪些?
定義一個業務類,類里面定義幾個方法,然后在執行這個方法的時候,會動態實現方法的耗時統計。
看業務類定義:
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_RETURN_VALUE_VAR + ";");}bodyStr.append(START_TIME);if (hasReturnType) {bodyStr.append("\n").append(METHOD_RETURN_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_RETURN_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(0;CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod);return ctClass.toBytecode(0;}} 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(0;String newName = oldMethodName + "$impl";oldMethod.setName(newName);CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null);StringBuffer sb = newe StringBuffer();sb.append("{");sb.append("\nSystem.out.println(\"start to modify bytecode\"); \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,效果是一樣的!
總結
以上是生活随笔為你收集整理的JVM插桩之三:javaagent介绍及javassist介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM插桩之二:Java agent基础
- 下一篇: JVM插码之六:jacoco插码及问题“