故事的小黃花
團隊中有同事在做性能優化相關的工作,因為公司基礎設施不足,同事在代碼中寫了大量的代碼統計某個方法的耗時,大概的代碼形式就是
@Override publicvoidmethod(Reqreq){ StopWatchstopWatch=newStopWatch(); stopWatch.start("某某方法-耗時統計"); method() stopWatch.stop(); log.info("查詢耗時分布:{}",stopWatch.prettyPrint()); }
這樣的代碼非常多,侵入性很大,聯想到之前學習的Java Agent技術,可以無侵入式地解決這類問題,所以做了一個很小很小的demo
Instrumentation
在了解Agent之前需要先看看Instrumentation
JDK從1.5版本開始引入了java.lang.instrument包,該包提供了一些工具幫助開發人員實現字節碼增強,Instrumentation接口的常用方法如下
publicinterfaceInstrumentation{ /** *注冊Class文件轉換器,轉換器用于改變Class文件二進制流的數據 * *@paramtransformer注冊的轉換器 *@paramcanRetransform設置是否允許重新轉換 */ voidaddTransformer(ClassFileTransformertransformer,booleancanRetransform); /** *移除一個轉換器 * *@paramtransformer需要移除的轉換器 */ booleanremoveTransformer(ClassFileTransformertransformer); /** *在類加載之后,重新轉換類,如果重新轉換的方法有活躍的棧幀,那些活躍的棧幀繼續運行未轉換前的方法 * *@param重新轉換的類數組 */ voidretransformClasses(Class>...classes)throwsUnmodifiableClassException; /** *當前JVM配置是否支持重新轉換 */ booleanisRetransformClassesSupported(); /** *獲取所有已加載的類 */ @SuppressWarnings("rawtypes") Class[]getAllLoadedClasses(); } publicinterfaceClassFileTransformer{ //className參數表示當前加載類的類名,classfileBuffer參數是待加載類文件的字節數組 //調用addTransformer注冊ClassFileTransformer以后,后續所有JVM加載類都會被它的transform方法攔截 //這個方法接收原類文件的字節數組,在這個方法中做類文件改寫,最后返回轉換過的字節數組,由JVM加載這個修改過的類文件 //如果transform方法返回null,表示不對此類做處理,如果返回值不為null,JVM會用返回的字節數組替換原來類的字節數組 byte[]transform(ClassLoaderloader, StringclassName, Class>classBeingRedefined, ProtectionDomainprotectionDomain, byte[]classfileBuffer) throwsIllegalClassFormatException; }
Instrumentation有兩種使用方式
在JVM啟動的時候添加一個Agent jar包
JVM運行以后在任意時刻通過Attach API遠程加載Agent的jar包
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
Agent
使用Java Agent需要借助一個方法,該方法的方法簽名如下
publicstaticvoidpremain(StringagentArgs,Instrumentationinstrumentation){
}
從字面上理解,就是運行在main()函數之前的類。在Java虛擬機啟動時,在執行main()函數之前,會先運行指定類的premain()方法,在premain()方法中對class文件進行修改,它有兩個入參
agentArgs:啟動參數,在JVM啟動時指定
instrumentation:上文所將的Instrumentation的實例,我們可以在方法中調用上文所講的方法,注冊對應的Class轉換器,對Class文件進行修改
如下圖,借助Instrumentation,JVM啟動時的處理流程是這樣的:JVM會執行指定類的premain()方法,在premain()中可以調用Instrumentation對象的addTransformer方法注冊ClassFileTransformer。當JVM加載類時會將類文件的字節數組傳遞給ClassFileTransformer的transform方法,在transform方法中對Class文件進行解析和修改,之后JVM就會加載轉換后的Class文件
JVM啟動時的處理流程
那我們需要做的就是寫一個轉換Class文件的ClassFileTransformer,下面用一個計算函數耗時的小例子看看Java Agent是怎么使用的
publicclassMyClassFileTransformerimplementsClassFileTransformer{ @Override publicbyte[]transform(ClassLoaderloader,StringclassName,Class>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer){ if("com/example/aop/agent/MyTest".equals(className)){ //使用ASM框架進行字節碼轉換 ClassReadercr=newClassReader(classfileBuffer); ClassWritercw=newClassWriter(cr,ClassWriter.COMPUTE_FRAMES); ClassVisitorcv=newTimeStatisticsVisitor(Opcodes.ASM7,cw); cr.accept(cv,ClassReader.SKIP_FRAMES|ClassReader.SKIP_DEBUG); returncw.toByteArray(); } returnclassfileBuffer; } } publicclassTimeStatisticsVisitorextendsClassVisitor{ publicTimeStatisticsVisitor(intapi,ClassVisitorclassVisitor){ super(Opcodes.ASM7,classVisitor); } @Override publicMethodVisitorvisitMethod(intaccess,Stringname,Stringdescriptor,Stringsignature,String[]exceptions){ MethodVisitormv=cv.visitMethod(access,name,descriptor,signature,exceptions); if(name.equals("")){ returnmv; } returnnewTimeStatisticsAdapter(api,mv,access,name,descriptor); } } publicclassTimeStatisticsAdapterextendsAdviceAdapter{ protectedTimeStatisticsAdapter(intapi,MethodVisitormethodVisitor,intaccess,Stringname,Stringdescriptor){ super(api,methodVisitor,access,name,descriptor); } @Override protectedvoidonMethodEnter(){ //進入函數時調用TimeStatistics的靜態方法start super.visitMethodInsn(Opcodes.INVOKESTATIC,"com/example/aop/agent/TimeStatistics","start","()V",false); super.onMethodEnter(); } @Override protectedvoidonMethodExit(intopcode){ //退出函數時調用TimeStatistics的靜態方法end super.onMethodExit(opcode); super.visitMethodInsn(Opcodes.INVOKESTATIC,"com/example/aop/agent/TimeStatistics","end","()V",false); } } publicclassTimeStatistics{ publicstaticThreadLocal t=newThreadLocal<>(); publicstaticvoidstart(){ t.set(System.currentTimeMillis()); } publicstaticvoidend(){ longtime=System.currentTimeMillis()-t.get(); System.out.println(Thread.currentThread().getStackTrace()[2]+"spend:"+time); } } publicclassAgentMain{ //premain()函數中注冊MyClassFileTransformer轉換器 publicstaticvoidpremain(StringagentArgs,Instrumentationinstrumentation){ System.out.println("premain方法"); instrumentation.addTransformer(newMyClassFileTransformer(),true); } } org.apache.maven.plugins maven-assembly-plugin 3.1.1 jar-with-dependencies //指定premain()的所在方法 com.example.aop.agent.AgentMain com.example.aop.agent.AgentMain true true package single org.apache.maven.plugins maven-compiler-plugin 3.1 ${maven.compiler.target}
使用命令行執行下面的測試類
java-javaagent:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jarcom.example.aop.agent.MyTest publicclassMyTest{ publicstaticvoidmain(String[]args)throwsInterruptedException{ Thread.sleep(3000); } }
計算出了某個方法的耗時
計算出某個方法的耗時
Attach
在上面的例子中,我們只能在JVM啟動時指定一個Agent,這種方式局限在main()方法執行前,如果我們想在項目啟動后隨時隨地地修改Class文件,要怎么辦呢?這個時候需要借助Java Agent的另外一個方法,該方法的簽名如下
publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){
}
agentmain()的參數與premain()有著同樣的含義,但是agentmain()是在Java Agent被Attach到Java虛擬機上時執行的,當Java Agent被attach到Java虛擬機上,Java程序的main()函數一般已經啟動,并且程序很可能已經運行了相當長的時間,此時通過Instrumentation.retransformClasses()方法,可以動態轉換Class文件并使之生效,下面用一個小例子演示一下這個功能
下面的類啟動后,會不斷打印出100這個數字,我們通過Attach功能使之打印出50這個數字
publicclassPrintNumTest{
publicstaticvoidmain(String[]args)throwsInterruptedException{ while(true){ System.out.println(getNum()); Thread.sleep(3000); } } privatestaticintgetNum(){ return100; } }
依然是定義一個ClassFileTransformer,使用ASM框架修改getNum()方法
publicclassPrintNumTransformerimplementsClassFileTransformer{ @Override publicbyte[]transform(ClassLoaderloader,StringclassName,Class>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{ if("com/example/aop/agent/PrintNumTest".equals(className)){ System.out.println("asm"); ClassReadercr=newClassReader(classfileBuffer); ClassWritercw=newClassWriter(cr,ClassWriter.COMPUTE_FRAMES); ClassVisitorcv=newTransformPrintNumVisitor(Opcodes.ASM7,cw); cr.accept(cv,ClassReader.SKIP_FRAMES|ClassReader.SKIP_DEBUG); returncw.toByteArray(); } returnclassfileBuffer; } } publicclassTransformPrintNumVisitorextendsClassVisitor{ publicTransformPrintNumVisitor(intapi,ClassVisitorclassVisitor){ super(Opcodes.ASM7,classVisitor); } @Override publicMethodVisitorvisitMethod(intaccess,Stringname,Stringdescriptor,Stringsignature,String[]exceptions){ MethodVisitormv=cv.visitMethod(access,name,descriptor,signature,exceptions); if(name.equals("getNum")){ returnnewTransformPrintNumAdapter(api,mv,access,name,descriptor); } returnmv; } } publicclassTransformPrintNumAdapterextendsAdviceAdapter{ protectedTransformPrintNumAdapter(intapi,MethodVisitormethodVisitor,intaccess,Stringname,Stringdescriptor){ super(api,methodVisitor,access,name,descriptor); } @Override protectedvoidonMethodEnter(){ super.visitIntInsn(BIPUSH,50); super.visitInsn(IRETURN); } } publicclassPrintNumAgent{ publicstaticvoidagentmain(StringagentArgs,Instrumentationinst)throwsUnmodifiableClassException{ System.out.println("agentmain"); inst.addTransformer(newPrintNumTransformer(),true); Class[]allLoadedClasses=inst.getAllLoadedClasses(); for(ClassallLoadedClass:allLoadedClasses){ if(allLoadedClass.getSimpleName().equals("PrintNumTest")){ System.out.println("Reloading:"+allLoadedClass.getName()); inst.retransformClasses(allLoadedClass); break; } } } }org.apache.maven.plugins maven-assembly-plugin 3.1.1 jar-with-dependencies //指定agentmain所在的類 com.example.aop.agent.PrintNumAgent com.example.aop.agent.PrintNumAgent true true package single org.apache.maven.plugins maven-compiler-plugin 3.1 ${maven.compiler.target}
因為是跨進程通信,Attach的發起端是一個獨立的java程序,這個java程序會調用VirtualMachine.attach方法開始合目標JVM進行跨進程通信
publicclassMyAttachMain{
publicstaticvoidmain(String[]args)throwsIOException,AttachNotSupportedException,AgentLoadException,AgentInitializationException{ VirtualMachinevirtualMachine=VirtualMachine.attach(args[0]); try{ virtualMachine.loadAgent("/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar"); }finally{ virtualMachine.detach(); } } }
使用jps查詢到PrintNumTest的進程id,再用下面的命令執行MyAttachMain類
java-cp/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home/lib/tools.jar:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jarcom.example.aop.agent.MyAttachMain49987
可以清楚地看到打印的數字變成了50
效果
Arthas
以上是我寫的小demo,有很多不足之處,看看大佬是怎么寫的,arthas的trace命令可以統計方法耗時,如下圖
Arthas
搭建調試環境
Arthas debug需要借助IDEA的遠程debug功能,可以參考 https://github.com/alibaba/arthas/issues/222
先寫一個可以循環執行的Demo
publicclassArthasTest{ publicstaticvoidmain(String[]args)throwsInterruptedException{ inti=0; while(true){ Thread.sleep(2000); print(i++); } } publicstaticvoidprint(Integercontent){ System.out.println("Mainprint:"+content); } }
命令行執行改demo
java-Xdebug-Xrunjdwp:transport=dt_socket,server=y,address=8000com.example.aop.agent.ArthasTest
在Arthas源碼的項目中設置遠程debug
在Arthas源碼的項目中設置遠程debug
在Arthas源碼的項目中設置遠程debug
在這個方法com.taobao.arthas.agent334.AgentBootstrap#main任意位置打上斷點,切換到剛剛設置的遠程debug模式,啟動項目
遠程debug模式
可以看到剛剛處于Listening的ArthasTest開始執行,啟動arthas-boot.jar,就可以看到斷點跳進Arthas源碼的項目中
跳進Arthas源碼的項目中
bytekit
在看trace命令之前需要一點前置知識,使用ASM進行字節碼增強,代碼邏輯不好修改,理解困難,所以bytekit基于ASM提供了一套簡潔的API,讓開發人員可以比較輕松地完成字節碼增強,我們先來看一個簡單的demo,來自https://github.com/alibaba/bytekit
publicclassSampleInterceptor{ @AtEnter(inline=false,suppress=RuntimeException.class,suppressHandler=PrintExceptionSuppressHandler.class) publicstaticvoidatEnter(@Binding.ThisObjectobject, @Binding.ClassObjectclazz, @Binding.ArgsObject[]args, @Binding.MethodNameStringmethodName, @Binding.MethodDescStringmethodDesc){ System.out.println("atEnter,args[0]:"+args[0]); } @AtExit(inline=true) publicstaticvoidatExit(@Binding.ReturnObjectreturnObject){ System.out.println("atExit,returnObject:"+returnObject); } @AtExceptionExit(inline=true,onException=RuntimeException.class) publicstaticvoidatExceptionExit(@Binding.ThrowableRuntimeExceptionex, @Binding.Field(name="exceptionCount")intexceptionCount){ System.out.println("atExceptionExit,ex:"+ex.getMessage()+",fieldexceptionCount:"+exceptionCount); } }
上文說過,bytekit的宗旨是提供簡介的API讓開發可以輕松地完成字節碼增強,從注解名我們就可以知道@AtEnter是在方法進入時插入,@AtExit是在方法退出時插入,@AtExceptionExit時在發生異常退出時插入
inline = true表示方法中的代碼直接插入增強方法中,inline = false表示是調用這個方法,有點難理解,我們等下看反編譯后的代碼
配置了 suppress = RuntimeException.class 和 suppressHandler = PrintExceptionSuppressHandler.class,說明插入的代碼會被 try/catch 包圍
@AtExceptionExit在原方法體范圍try-catch指定異常進行處理
這是我們要進行增強的方法
publicclassSample{ privateintexceptionCount=0; publicStringhello(Stringstr,booleanexception){ if(exception){ exceptionCount++; thrownewRuntimeException("testexception,str:"+str); } return"hello"+str; } } publicclassSampleMain{ publicstaticvoidmain(String[]args)throwsException{ //解析定義的Interceptor類和相關的注解 DefaultInterceptorClassParserinterceptorClassParser=newDefaultInterceptorClassParser(); Listprocessors=interceptorClassParser.parse(SampleInterceptor.class); //加載字節碼 ClassNodeclassNode=AsmUtils.loadClass(Sample.class); //對加載到的字節碼做增強處理 for(MethodNodemethodNode:classNode.methods){ if(methodNode.name.equals("hello")){ MethodProcessormethodProcessor=newMethodProcessor(classNode,methodNode); for(InterceptorProcessorinterceptor:processors){ interceptor.process(methodProcessor); } } } //獲取增強后的字節碼 byte[]bytes=AsmUtils.toBytes(classNode); //查看反編譯結果 System.out.println(Decompiler.decompile(bytes)); //修改Sample AgentUtils.reTransform(Sample.class,bytes); //執行sample的方法 try{ Samplesample=newSample(); sample.hello("3",false); sample.hello("4",true); }catch(Exceptione){ e.printStackTrace(); } } }
這是Sample反編譯后的結果,代碼量劇增
publicclassSample{ privateintexceptionCount=0; /* *WARNING-voiddeclaration */ publicStringhello(Stringstring,booleanbl){ try{ Stringstring2; voidstr; voidexception; try{ //@AtEnter直接調用,inline為false的效果 SampleInterceptor.atEnter((Object)this,Sample.class,(Object[])newObject[]{string,newBoolean(bl)},(String)"hello",(String)"(Ljava/lang/String;Z)Ljava/lang/String;"); } catch(RuntimeExceptionruntimeException){ Classclazz=Sample.class; RuntimeExceptionruntimeException2=runtimeException; System.out.println("exceptionhandler:"+clazz); runtimeException2.printStackTrace(); } if(exception!=false){ ++this.exceptionCount; thrownewRuntimeException("testexception,str:"+(String)str); } Stringstring3=string2="hello"+(String)str; //@AtExit代碼直接插入 System.out.println("atExit,returnObject:"+string3); returnstring2; } catch(RuntimeExceptionruntimeException){ intn=this.exceptionCount; RuntimeExceptionruntimeException3=runtimeException; //@AtExceptionExit代碼直接插入 System.out.println("atExceptionExit,ex:"+runtimeException3.getMessage()+",fieldexceptionCount:"+n); throwruntimeException; } } }
有了這個前置知識,我們來看看trace命令
trace
trace
Arthas命令很多,如果是exit、logout、quit、jobs、fg、bg、kill等簡單的命令,就會直接執行,如果是trace這種復雜的命令,會專門用一個類寫處理的邏輯,如上圖,根據名字就可以猜到這個類是處理什么命令的,這么多類的組織形式是模版模式,入口在com.taobao.arthas.core.shell.command.AnnotatedCommand#process,
publicabstractclassAnnotatedCommand{ publicabstractvoidprocess(CommandProcessprocess); } publicclassTraceCommandextendsEnhancerCommand{ } publicabstractclassEnhancerCommandextendsAnnotatedCommand{ @Override publicvoidprocess(finalCommandProcessprocess){ //ctrl-Csupport process.interruptHandler(newCommandInterruptHandler(process)); //qexitsupport process.stdinHandler(newQExitHandler(process)); //starttoenhance enhance(process); } }
有一些命令都有字節碼增強的邏輯,這些邏輯共同封裝在了EnhancerCommand這個類中,TraceCommand繼承了EnhancerCommand,當trace命令執行的時候,增強的邏輯在EnhancerCommand,我們只看核心代碼
com.taobao.arthas.core.command.monitor200.EnhancerCommand#enhance com.taobao.arthas.core.advisor.Enhancer#enhance(java.lang.instrument.Instrumentation) publicsynchronizedEnhancerAffectenhance(finalInstrumentationinst)throwsUnmodifiableClassException{ ...... try{ //很明顯,這里添加了一個文件轉換器,注意,此處的轉換器為本類 ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this,isTracing); ...... }catch(Throwablee){ logger.error("Enhancererror,matchingClasses:{}",matchingClasses,e); affect.setThrowable(e); } returnaffect; }
根據方法名就可以在本類搜索到,具體代碼如下
@Override publicbyte[]transform(finalClassLoaderinClassLoader,StringclassName,Class>classBeingRedefined, ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{ try{ //檢查classloader能否加載到SpyAPI,如果不能,則放棄增強 try{ if(inClassLoader!=null){ inClassLoader.loadClass(SpyAPI.class.getName()); } }catch(Throwablee){ logger.error("theclassloadercannotloadSpyAPI,ignoreit.classloader:{},className:{}", inClassLoader.getClass().getName(),className,e); returnnull; } //這里要再次過濾一次,為啥?因為在transform的過程中,有可能還會再誕生新的類 //所以需要將之前需要轉換的類集合傳遞下來,再次進行判斷 if(matchingClasses!=null&&!matchingClasses.contains(classBeingRedefined)){ returnnull; } //ClassNode中有各種屬性,對應Class文件結構 //keeporiginclassreaderforbytecodeoptimizations,avoidingJVMmetaspaceOOM. ClassNodeclassNode=newClassNode(Opcodes.ASM9); ClassReaderclassReader=AsmUtils.toClassNode(classfileBuffer,classNode); //removeJSRhttps://github.com/alibaba/arthas/issues/1304 classNode=AsmUtils.removeJSRInstructions(classNode); //重要代碼,生成增強字節碼的攔截器 DefaultInterceptorClassParserdefaultInterceptorClassParser=newDefaultInterceptorClassParser(); finalListinterceptorProcessors=newArrayList (); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class)); if(this.isTracing){ //根據配置判斷trace命令是否要跳過計算Java類庫的代碼的耗時 if(!this.skipJDKTrace){ interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor1.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor2.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor3.class)); }else{ interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor1.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor2.class)); interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor3.class)); } } List matchedMethods=newArrayList (); for(MethodNodemethodNode:classNode.methods){ if(!isIgnore(methodNode,methodNameMatcher)){ matchedMethods.add(methodNode); } } //https://github.com/alibaba/arthas/issues/1690 if(AsmUtils.isEnhancerByCGLIB(className)){ for(MethodNodemethodNode:matchedMethods){ if(AsmUtils.isConstructor(methodNode)){ AsmUtils.fixConstructorExceptionTable(methodNode); } } } ....... for(MethodNodemethodNode:matchedMethods){ if(AsmUtils.isNative(methodNode)){ logger.info("ignorenativemethod:{}", AsmUtils.methodDeclaration(Type.getObjectType(classNode.name),methodNode)); continue; } //先查找是否有atBeforeInvoke函數,如果有,則說明已經有trace了,則直接不再嘗試增強,直接插入listener if(AsmUtils.containsMethodInsnNode(methodNode,Type.getInternalName(SpyAPI.class),"atBeforeInvoke")){ for(AbstractInsnNodeinsnNode=methodNode.instructions.getFirst();insnNode!=null;insnNode=insnNode .getNext()){ if(insnNodeinstanceofMethodInsnNode){ finalMethodInsnNodemethodInsnNode=(MethodInsnNode)insnNode; if(this.skipJDKTrace){ if(methodInsnNode.owner.startsWith("java/")){ continue; } } //原始類型的box類型相關的都跳過 if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))){ continue; } AdviceListenerManager.registerTraceAdviceListener(inClassLoader,className, methodInsnNode.owner,methodInsnNode.name,methodInsnNode.desc,listener); } } }else{ //重點代碼,增強動作就是在這里完成的 MethodProcessormethodProcessor=newMethodProcessor(classNode,methodNode,groupLocationFilter); for(InterceptorProcessorinterceptor:interceptorProcessors){ try{ List locations=interceptor.process(methodProcessor); for(Locationlocation:locations){ if(locationinstanceofMethodInsnNodeWare){ MethodInsnNodeWaremethodInsnNodeWare=(MethodInsnNodeWare)location; MethodInsnNodemethodInsnNode=methodInsnNodeWare.methodInsnNode(); AdviceListenerManager.registerTraceAdviceListener(inClassLoader,className, methodInsnNode.owner,methodInsnNode.name,methodInsnNode.desc,listener); } } }catch(Throwablee){ logger.error("enhancererror,class:{},method:{},interceptor:{}",classNode.name,methodNode.name,interceptor.getClass().getName(),e); } } } //enter/exist總是要插入listener AdviceListenerManager.registerAdviceListener(inClassLoader,className,methodNode.name,methodNode.desc, listener); affect.addMethodAndCount(inClassLoader,className,methodNode.name,methodNode.desc); } //https://github.com/alibaba/arthas/issues/1223,V1_5的majorversion是49 if(AsmUtils.getMajorVersion(classNode.version)49)?{ ????????classNode.version?=?AsmUtils.setMajorVersion(classNode.version,?49); ??????} ??????byte[]?enhanceClassByteArray?=?AsmUtils.toBytes(classNode,?inClassLoader,?classReader); ??????//?增強成功,記錄類 ??????classBytesCache.put(classBeingRedefined,?new?Object()); ??????//?dump?the?class ??????dumpClassIfNecessary(className,?enhanceClassByteArray,?affect); ??????//?成功計數 ??????affect.cCnt(1); ??????return?enhanceClassByteArray; ????}?catch?(Throwable?t)?{ ??????logger.warn("transform?loader[{}]:class[{}]?failed.",?inClassLoader,?className,?t); ??????affect.setThrowable(t); ????} ????return?null; }
這段代碼很長,其實主要邏輯就兩個
解析Interceptor Class的@AtXxx,@Binding等注解,生成InterceptorProcessor對象集合
遍歷InterceptorProcessor集合,修改原方法的字節碼
整體的流程如下圖
整體的流程如圖
那這些攔截器長什么樣子呢?我們隨便找一個例子來看看
publicstaticclassSpyInterceptor1{ @AtEnter(inline=true) publicstaticvoidatEnter(@Binding.ThisObjecttarget,@Binding.ClassClass>clazz, @Binding.MethodInfoStringmethodInfo,@Binding.ArgsObject[]args){ SpyAPI.atEnter(clazz,methodInfo,target,args); } }
看到這里,就很熟悉了,跟上面bytekit的例子很像,是在方法進入時插入的,當然,這里只是淺講一下trace的原理,bytekit背后的原理,需要更底層的知識儲備,我還需要繼續學習
審核編輯:黃飛
-
JAVA
+關注
關注
19文章
2974瀏覽量
104977 -
API
+關注
關注
2文章
1510瀏覽量
62293 -
代碼
+關注
關注
30文章
4823瀏覽量
68900 -
JVM
+關注
關注
0文章
158瀏覽量
12252 -
虛擬機
+關注
關注
1文章
931瀏覽量
28360
原文標題:手把手教你實現一個Java Agent
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論