故事的小黃花
團(tuán)隊(duì)中有同事在做性能優(yōu)化相關(guān)的工作,因?yàn)楣净A(chǔ)設(shè)施不足,同事在代碼中寫了大量的代碼統(tǒng)計(jì)某個(gè)方法的耗時(shí),大概的代碼形式就是
@Override
publicvoidmethod(Reqreq){
StopWatchstopWatch=newStopWatch();
stopWatch.start("某某方法-耗時(shí)統(tǒng)計(jì)");
method()
stopWatch.stop();
log.info("查詢耗時(shí)分布:{}",stopWatch.prettyPrint());
}
這樣的代碼非常多,侵入性很大,聯(lián)想到之前學(xué)習(xí)的Java Agent技術(shù),可以無(wú)侵入式地解決這類問題,所以做了一個(gè)很小很小的demo
Instrumentation
在了解Agent之前需要先看看Instrumentation
JDK從1.5版本開始引入了java.lang.instrument包,該包提供了一些工具幫助開發(fā)人員實(shí)現(xiàn)字節(jié)碼增強(qiáng),Instrumentation接口的常用方法如下
publicinterfaceInstrumentation{
/**
*注冊(cè)Class文件轉(zhuǎn)換器,轉(zhuǎn)換器用于改變Class文件二進(jìn)制流的數(shù)據(jù)
*
*@paramtransformer注冊(cè)的轉(zhuǎn)換器
*@paramcanRetransform設(shè)置是否允許重新轉(zhuǎn)換
*/
voidaddTransformer(ClassFileTransformertransformer,booleancanRetransform);
/**
*移除一個(gè)轉(zhuǎn)換器
*
*@paramtransformer需要移除的轉(zhuǎn)換器
*/
booleanremoveTransformer(ClassFileTransformertransformer);
/**
*在類加載之后,重新轉(zhuǎn)換類,如果重新轉(zhuǎn)換的方法有活躍的棧幀,那些活躍的棧幀繼續(xù)運(yùn)行未轉(zhuǎn)換前的方法
*
*@param重新轉(zhuǎn)換的類數(shù)組
*/
voidretransformClasses(Class>...classes)throwsUnmodifiableClassException;
/**
*當(dāng)前JVM配置是否支持重新轉(zhuǎn)換
*/
booleanisRetransformClassesSupported();
/**
*獲取所有已加載的類
*/
@SuppressWarnings("rawtypes")
Class[]getAllLoadedClasses();
}
publicinterfaceClassFileTransformer{
//className參數(shù)表示當(dāng)前加載類的類名,classfileBuffer參數(shù)是待加載類文件的字節(jié)數(shù)組
//調(diào)用addTransformer注冊(cè)ClassFileTransformer以后,后續(xù)所有JVM加載類都會(huì)被它的transform方法攔截
//這個(gè)方法接收原類文件的字節(jié)數(shù)組,在這個(gè)方法中做類文件改寫,最后返回轉(zhuǎn)換過的字節(jié)數(shù)組,由JVM加載這個(gè)修改過的類文件
//如果transform方法返回null,表示不對(duì)此類做處理,如果返回值不為null,JVM會(huì)用返回的字節(jié)數(shù)組替換原來類的字節(jié)數(shù)組
byte[]transform(ClassLoaderloader,
StringclassName,
Class>classBeingRedefined,
ProtectionDomainprotectionDomain,
byte[]classfileBuffer)
throwsIllegalClassFormatException;
}
Instrumentation有兩種使用方式
在JVM啟動(dòng)的時(shí)候添加一個(gè)Agent jar包
JVM運(yùn)行以后在任意時(shí)刻通過Attach API遠(yuǎn)程加載Agent的jar包
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
Agent
使用Java Agent需要借助一個(gè)方法,該方法的方法簽名如下
publicstaticvoidpremain(StringagentArgs,Instrumentationinstrumentation){
}
從字面上理解,就是運(yùn)行在main()函數(shù)之前的類。在Java虛擬機(jī)啟動(dòng)時(shí),在執(zhí)行main()函數(shù)之前,會(huì)先運(yùn)行指定類的premain()方法,在premain()方法中對(duì)class文件進(jìn)行修改,它有兩個(gè)入?yún)?/span>
agentArgs:?jiǎn)?dòng)參數(shù),在JVM啟動(dòng)時(shí)指定
instrumentation:上文所將的Instrumentation的實(shí)例,我們可以在方法中調(diào)用上文所講的方法,注冊(cè)對(duì)應(yīng)的Class轉(zhuǎn)換器,對(duì)Class文件進(jìn)行修改
如下圖,借助Instrumentation,JVM啟動(dòng)時(shí)的處理流程是這樣的:JVM會(huì)執(zhí)行指定類的premain()方法,在premain()中可以調(diào)用Instrumentation對(duì)象的addTransformer方法注冊(cè)ClassFileTransformer。當(dāng)JVM加載類時(shí)會(huì)將類文件的字節(jié)數(shù)組傳遞給ClassFileTransformer的transform方法,在transform方法中對(duì)Class文件進(jìn)行解析和修改,之后JVM就會(huì)加載轉(zhuǎn)換后的Class文件

JVM啟動(dòng)時(shí)的處理流程
那我們需要做的就是寫一個(gè)轉(zhuǎn)換Class文件的ClassFileTransformer,下面用一個(gè)計(jì)算函數(shù)耗時(shí)的小例子看看Java Agent是怎么使用的
publicclassMyClassFileTransformerimplementsClassFileTransformer{
@Override
publicbyte[]transform(ClassLoaderloader,StringclassName,Class>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer){
if("com/example/aop/agent/MyTest".equals(className)){
//使用ASM框架進(jìn)行字節(jié)碼轉(zhuǎn)換
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(){
//進(jìn)入函數(shù)時(shí)調(diào)用TimeStatistics的靜態(tài)方法start
super.visitMethodInsn(Opcodes.INVOKESTATIC,"com/example/aop/agent/TimeStatistics","start","()V",false);
super.onMethodEnter();
}
@Override
protectedvoidonMethodExit(intopcode){
//退出函數(shù)時(shí)調(diào)用TimeStatistics的靜態(tài)方法end
super.onMethodExit(opcode);
super.visitMethodInsn(Opcodes.INVOKESTATIC,"com/example/aop/agent/TimeStatistics","end","()V",false);
}
}
publicclassTimeStatistics{
publicstaticThreadLocalt=newThreadLocal<>();
publicstaticvoidstart(){
t.set(System.currentTimeMillis());
}
publicstaticvoidend(){
longtime=System.currentTimeMillis()-t.get();
System.out.println(Thread.currentThread().getStackTrace()[2]+"spend:"+time);
}
}
publicclassAgentMain{
//premain()函數(shù)中注冊(cè)MyClassFileTransformer轉(zhuǎn)換器
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.source}
${maven.compiler.target}
使用命令行執(zhí)行下面的測(cè)試類
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); } }
計(jì)算出了某個(gè)方法的耗時(shí)

計(jì)算出某個(gè)方法的耗時(shí)
Attach
在上面的例子中,我們只能在JVM啟動(dòng)時(shí)指定一個(gè)Agent,這種方式局限在main()方法執(zhí)行前,如果我們想在項(xiàng)目啟動(dòng)后隨時(shí)隨地地修改Class文件,要怎么辦呢?這個(gè)時(shí)候需要借助Java Agent的另外一個(gè)方法,該方法的簽名如下
publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){
}
agentmain()的參數(shù)與premain()有著同樣的含義,但是agentmain()是在Java Agent被Attach到Java虛擬機(jī)上時(shí)執(zhí)行的,當(dāng)Java Agent被attach到Java虛擬機(jī)上,Java程序的main()函數(shù)一般已經(jīng)啟動(dòng),并且程序很可能已經(jīng)運(yùn)行了相當(dāng)長(zhǎng)的時(shí)間,此時(shí)通過Instrumentation.retransformClasses()方法,可以動(dòng)態(tài)轉(zhuǎn)換Class文件并使之生效,下面用一個(gè)小例子演示一下這個(gè)功能
下面的類啟動(dòng)后,會(huì)不斷打印出100這個(gè)數(shù)字,我們通過Attach功能使之打印出50這個(gè)數(shù)字
publicclassPrintNumTest{
publicstaticvoidmain(String[]args)throwsInterruptedException{
while(true){
System.out.println(getNum());
Thread.sleep(3000);
}
}
privatestaticintgetNum(){
return100;
}
}
依然是定義一個(gè)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.source}
${maven.compiler.target}
因?yàn)槭强邕M(jìn)程通信,Attach的發(fā)起端是一個(gè)獨(dú)立的java程序,這個(gè)java程序會(huì)調(diào)用VirtualMachine.attach方法開始合目標(biāo)JVM進(jìn)行跨進(jìn)程通信
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的進(jìn)程id,再用下面的命令執(zhí)行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
可以清楚地看到打印的數(shù)字變成了50

效果
Arthas
以上是我寫的小demo,有很多不足之處,看看大佬是怎么寫的,arthas的trace命令可以統(tǒng)計(jì)方法耗時(shí),如下圖

Arthas
搭建調(diào)試環(huán)境
Arthas debug需要借助IDEA的遠(yuǎn)程debug功能,可以參考 https://github.com/alibaba/arthas/issues/222
先寫一個(gè)可以循環(huán)執(zhí)行的Demo
publicclassArthasTest{
publicstaticvoidmain(String[]args)throwsInterruptedException{
inti=0;
while(true){
Thread.sleep(2000);
print(i++);
}
}
publicstaticvoidprint(Integercontent){
System.out.println("Mainprint:"+content);
}
}
命令行執(zhí)行改demo
java-Xdebug-Xrunjdwp:transport=dt_socket,server=y,address=8000com.example.aop.agent.ArthasTest
在Arthas源碼的項(xiàng)目中設(shè)置遠(yuǎn)程debug

在Arthas源碼的項(xiàng)目中設(shè)置遠(yuǎn)程debug

在Arthas源碼的項(xiàng)目中設(shè)置遠(yuǎn)程debug
在這個(gè)方法com.taobao.arthas.agent334.AgentBootstrap#main任意位置打上斷點(diǎn),切換到剛剛設(shè)置的遠(yuǎn)程debug模式,啟動(dòng)項(xiàng)目

遠(yuǎn)程debug模式
可以看到剛剛處于Listening的ArthasTest開始執(zhí)行,啟動(dòng)arthas-boot.jar,就可以看到斷點(diǎn)跳進(jìn)Arthas源碼的項(xiàng)目中

跳進(jìn)Arthas源碼的項(xiàng)目中
bytekit
在看trace命令之前需要一點(diǎn)前置知識(shí),使用ASM進(jìn)行字節(jié)碼增強(qiáng),代碼邏輯不好修改,理解困難,所以bytekit基于ASM提供了一套簡(jiǎn)潔的API,讓開發(fā)人員可以比較輕松地完成字節(jié)碼增強(qiáng),我們先來看一個(gè)簡(jiǎn)單的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的宗旨是提供簡(jiǎn)介的API讓開發(fā)可以輕松地完成字節(jié)碼增強(qiáng),從注解名我們就可以知道@AtEnter是在方法進(jìn)入時(shí)插入,@AtExit是在方法退出時(shí)插入,@AtExceptionExit時(shí)在發(fā)生異常退出時(shí)插入
inline = true表示方法中的代碼直接插入增強(qiáng)方法中,inline = false表示是調(diào)用這個(gè)方法,有點(diǎn)難理解,我們等下看反編譯后的代碼
配置了 suppress = RuntimeException.class 和 suppressHandler = PrintExceptionSuppressHandler.class,說明插入的代碼會(huì)被 try/catch 包圍
@AtExceptionExit在原方法體范圍try-catch指定異常進(jìn)行處理
這是我們要進(jìn)行增強(qiáng)的方法
publicclassSample{
privateintexceptionCount=0;
publicStringhello(Stringstr,booleanexception){
if(exception){
exceptionCount++;
thrownewRuntimeException("testexception,str:"+str);
}
return"hello"+str;
}
}
publicclassSampleMain{
publicstaticvoidmain(String[]args)throwsException{
//解析定義的Interceptor類和相關(guān)的注解
DefaultInterceptorClassParserinterceptorClassParser=newDefaultInterceptorClassParser();
Listprocessors=interceptorClassParser.parse(SampleInterceptor.class);
//加載字節(jié)碼
ClassNodeclassNode=AsmUtils.loadClass(Sample.class);
//對(duì)加載到的字節(jié)碼做增強(qiáng)處理
for(MethodNodemethodNode:classNode.methods){
if(methodNode.name.equals("hello")){
MethodProcessormethodProcessor=newMethodProcessor(classNode,methodNode);
for(InterceptorProcessorinterceptor:processors){
interceptor.process(methodProcessor);
}
}
}
//獲取增強(qiáng)后的字節(jié)碼
byte[]bytes=AsmUtils.toBytes(classNode);
//查看反編譯結(jié)果
System.out.println(Decompiler.decompile(bytes));
//修改Sample
AgentUtils.reTransform(Sample.class,bytes);
//執(zhí)行sample的方法
try{
Samplesample=newSample();
sample.hello("3",false);
sample.hello("4",true);
}catch(Exceptione){
e.printStackTrace();
}
}
}
這是Sample反編譯后的結(jié)果,代碼量劇增
publicclassSample{
privateintexceptionCount=0;
/*
*WARNING-voiddeclaration
*/
publicStringhello(Stringstring,booleanbl){
try{
Stringstring2;
voidstr;
voidexception;
try{
//@AtEnter直接調(diào)用,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;
}
}
}
有了這個(gè)前置知識(shí),我們來看看trace命令
trace

trace
Arthas命令很多,如果是exit、logout、quit、jobs、fg、bg、kill等簡(jiǎn)單的命令,就會(huì)直接執(zhí)行,如果是trace這種復(fù)雜的命令,會(huì)專門用一個(gè)類寫處理的邏輯,如上圖,根據(jù)名字就可以猜到這個(gè)類是處理什么命令的,這么多類的組織形式是模版模式,入口在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);
}
}
有一些命令都有字節(jié)碼增強(qiáng)的邏輯,這些邏輯共同封裝在了EnhancerCommand這個(gè)類中,TraceCommand繼承了EnhancerCommand,當(dāng)trace命令執(zhí)行的時(shí)候,增強(qiáng)的邏輯在EnhancerCommand,我們只看核心代碼
com.taobao.arthas.core.command.monitor200.EnhancerCommand#enhance
com.taobao.arthas.core.advisor.Enhancer#enhance(java.lang.instrument.Instrumentation)
publicsynchronizedEnhancerAffectenhance(finalInstrumentationinst)throwsUnmodifiableClassException{
......
try{
//很明顯,這里添加了一個(gè)文件轉(zhuǎn)換器,注意,此處的轉(zhuǎn)換器為本類
ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this,isTracing);
......
}catch(Throwablee){
logger.error("Enhancererror,matchingClasses:{}",matchingClasses,e);
affect.setThrowable(e);
}
returnaffect;
}
根據(jù)方法名就可以在本類搜索到,具體代碼如下
@Override
publicbyte[]transform(finalClassLoaderinClassLoader,StringclassName,Class>classBeingRedefined,
ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{
try{
//檢查classloader能否加載到SpyAPI,如果不能,則放棄增強(qiáng)
try{
if(inClassLoader!=null){
inClassLoader.loadClass(SpyAPI.class.getName());
}
}catch(Throwablee){
logger.error("theclassloadercannotloadSpyAPI,ignoreit.classloader:{},className:{}",
inClassLoader.getClass().getName(),className,e);
returnnull;
}
//這里要再次過濾一次,為啥?因?yàn)樵趖ransform的過程中,有可能還會(huì)再誕生新的類
//所以需要將之前需要轉(zhuǎn)換的類集合傳遞下來,再次進(jìn)行判斷
if(matchingClasses!=null&&!matchingClasses.contains(classBeingRedefined)){
returnnull;
}
//ClassNode中有各種屬性,對(duì)應(yīng)Class文件結(jié)構(gòu)
//keeporiginclassreaderforbytecodeoptimizations,avoidingJVMmetaspaceOOM.
ClassNodeclassNode=newClassNode(Opcodes.ASM9);
ClassReaderclassReader=AsmUtils.toClassNode(classfileBuffer,classNode);
//removeJSRhttps://github.com/alibaba/arthas/issues/1304
classNode=AsmUtils.removeJSRInstructions(classNode);
//重要代碼,生成增強(qiáng)字節(jié)碼的攔截器
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){
//根據(jù)配置判斷trace命令是否要跳過計(jì)算Java類庫(kù)的代碼的耗時(shí)
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));
}
}
ListmatchedMethods=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函數(shù),如果有,則說明已經(jīng)有trace了,則直接不再嘗試增強(qiáng),直接插入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類型相關(guān)的都跳過
if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))){
continue;
}
AdviceListenerManager.registerTraceAdviceListener(inClassLoader,className,
methodInsnNode.owner,methodInsnNode.name,methodInsnNode.desc,listener);
}
}
}else{
//重點(diǎn)代碼,增強(qiáng)動(dòng)作就是在這里完成的
MethodProcessormethodProcessor=newMethodProcessor(classNode,methodNode,groupLocationFilter);
for(InterceptorProcessorinterceptor:interceptorProcessors){
try{
Listlocations=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);
??????//?增強(qiáng)成功,記錄類
??????classBytesCache.put(classBeingRedefined,?new?Object());
??????//?dump?the?class
??????dumpClassIfNecessary(className,?enhanceClassByteArray,?affect);
??????//?成功計(jì)數(shù)
??????affect.cCnt(1);
??????return?enhanceClassByteArray;
????}?catch?(Throwable?t)?{
??????logger.warn("transform?loader[{}]:class[{}]?failed.",?inClassLoader,?className,?t);
??????affect.setThrowable(t);
????}
????return?null;
}
這段代碼很長(zhǎng),其實(shí)主要邏輯就兩個(gè)
解析Interceptor Class的@AtXxx,@Binding等注解,生成InterceptorProcessor對(duì)象集合
遍歷InterceptorProcessor集合,修改原方法的字節(jié)碼
整體的流程如下圖

整體的流程如圖
那這些攔截器長(zhǎng)什么樣子呢?我們隨便找一個(gè)例子來看看
publicstaticclassSpyInterceptor1{
@AtEnter(inline=true)
publicstaticvoidatEnter(@Binding.ThisObjecttarget,@Binding.ClassClass>clazz,
@Binding.MethodInfoStringmethodInfo,@Binding.ArgsObject[]args){
SpyAPI.atEnter(clazz,methodInfo,target,args);
}
}
看到這里,就很熟悉了,跟上面bytekit的例子很像,是在方法進(jìn)入時(shí)插入的,當(dāng)然,這里只是淺講一下trace的原理,bytekit背后的原理,需要更底層的知識(shí)儲(chǔ)備,我還需要繼續(xù)學(xué)習(xí)
審核編輯:黃飛
-
JAVA
+關(guān)注
關(guān)注
20文章
3001瀏覽量
116419 -
API
+關(guān)注
關(guān)注
2文章
2368瀏覽量
66752 -
代碼
+關(guān)注
關(guān)注
30文章
4967瀏覽量
73954 -
JVM
+關(guān)注
關(guān)注
0文章
161瀏覽量
13036 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
972瀏覽量
30463
原文標(biāo)題:手把手教你實(shí)現(xiàn)一個(gè)Java Agent
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
《零基礎(chǔ)開發(fā)AI Agent——手把手教你用扣子做智能體》
美女手把手教你如何裝機(jī)(中)
手把手教你安裝Quartus II
手把手教你學(xué)LabVIEW視覺設(shè)計(jì)
手把手教你開關(guān)電源PCB排板
手把手教你實(shí)現(xiàn)一個(gè)Java Agent(JVM啟動(dòng)時(shí)的處理流程)
評(píng)論