0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

手把手教你实现一个Java Agent(JVM启动时的处理流程)

jf_ro2CN3Fa 来源:稀土掘金 2023-11-27 10:53 次阅读

故事的小黄花

团队中有同事在做性能优化相关的工作,因为公司基础设施不足,同事在代码中写了大量的代码统计某个方法的耗时,大概的代码形式就是

@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,
ClassclassBeingRedefined,
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文件

55de5326-8c08-11ee-939d-92fbcf53809c.jpg

JVM启动时的处理流程

那我们需要做的就是写一个转换Class文件的ClassFileTransformer,下面用一个计算函数耗时的小例子看看Java Agent是怎么使用的

publicclassMyClassFileTransformerimplementsClassFileTransformer{
@Override
publicbyte[]transform(ClassLoaderloader,StringclassName,ClassclassBeingRedefined,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{
publicstaticThreadLocalt=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.source}
${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);
}
}

计算出了某个方法的耗时

55ebec5c-8c08-11ee-939d-92fbcf53809c.jpg

计算出某个方法的耗时

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,ClassclassBeingRedefined,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}




因为是跨进程通信,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

5611553c-8c08-11ee-939d-92fbcf53809c.jpg

效果

Arthas

以上是我写的小demo,有很多不足之处,看看大佬是怎么写的,arthas的trace命令可以统计方法耗时,如下图

56236cd6-8c08-11ee-939d-92fbcf53809c.jpg

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

564fa490-8c08-11ee-939d-92fbcf53809c.jpg

在Arthas源码的项目中设置远程debug

566441b6-8c08-11ee-939d-92fbcf53809c.jpg

在Arthas源码的项目中设置远程debug

在这个方法com.taobao.arthas.agent334.AgentBootstrap#main任意位置打上断点,切换到刚刚设置的远程debug模式,启动项目

56845488-8c08-11ee-939d-92fbcf53809c.jpg

远程debug模式

可以看到刚刚处于Listening的ArthasTest开始执行,启动arthas-boot.jar,就可以看到断点跳进Arthas源码的项目中

569d445c-8c08-11ee-939d-92fbcf53809c.jpg

跳进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

56bb8232-8c08-11ee-939d-92fbcf53809c.jpg

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,ClassclassBeingRedefined,
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));
}
}

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函数,如果有,则说明已经有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{
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);

      // 增强成功,记录类
      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集合,修改原方法的字节码

整体的流程如下图

56c73410-8c08-11ee-939d-92fbcf53809c.jpg

整体的流程如图

那这些拦截器长什么样子呢?我们随便找一个例子来看看

publicstaticclassSpyInterceptor1{
@AtEnter(inline=true)
publicstaticvoidatEnter(@Binding.ThisObjecttarget,@Binding.ClassClassclazz,
@Binding.MethodInfoStringmethodInfo,@Binding.ArgsObject[]args){
SpyAPI.atEnter(clazz,methodInfo,target,args);
}
}

看到这里,就很熟悉了,跟上面bytekit的例子很像,是在方法进入时插入的,当然,这里只是浅讲一下trace的原理,bytekit背后的原理,需要更底层的知识储备,我还需要继续学习

审核编辑:黄飞

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2956

    浏览量

    104531
  • API
    API
    +关注

    关注

    2

    文章

    1483

    浏览量

    61797
  • 代码
    +关注

    关注

    30

    文章

    4741

    浏览量

    68324
  • JVM
    JVM
    +关注

    关注

    0

    文章

    157

    浏览量

    12205
  • 虚拟机
    +关注

    关注

    1

    文章

    908

    浏览量

    28064

原文标题:手把手教你实现一个Java Agent

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    图文教程:手把手教你焊接贴片元件

    图文教程:手把手教你焊接贴片元件,首先来张全部焊接点的PCB图
    发表于 04-01 11:03 3.6w次阅读
    图文教程:<b class='flag-5'>手把手</b><b class='flag-5'>教你</b>焊接贴片元件

    手把手教你构建完整的工程

    手把手教你构建完整的工程
    发表于 08-03 09:54 33次下载
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>构建<b class='flag-5'>一</b><b class='flag-5'>个</b>完整的工程

    手把手教你写批处理-批处理的介绍

    手把手教你写批处理-批处理的介绍
    发表于 10-25 15:02 69次下载

    美女手把手教你如何装机(中)

    美女手把手教你如何装机(中) 再来是硬碟的部份,这款机壳还不错,可以旋转支架~
    发表于 01-27 11:14 1454次阅读

    美女手把手教你如何装机(下)

    美女手把手教你如何装机(下) 接著下来就是今天的重头戏,开核萝!~
    发表于 01-27 11:16 2915次阅读

    手把手教你学习FPGA—LED篇

    电子专业单片机相关知识学习教材资料——手把手教你学习FPGA—LED篇
    发表于 08-08 17:19 0次下载

    手把手教你学电子书制作

    手把手教你学电子书制作,可以自己DIY电子书
    发表于 09-13 11:26 0次下载

    手把手教你安装Quartus II

    本章手把手把教你如何安装 Quartus II 软件 ,并将它激活 。此外 还有USB -Blaster下载器的驱动安装步骤 。
    发表于 09-18 14:55 9次下载

    手把手教你在家搭建监控系统

    手把手教你在家搭建监控系统
    发表于 01-17 19:47 25次下载

    手把手教你做电子时钟---前言

    手把手教你做彩铃电子时钟
    发表于 11-14 16:53 11次下载

    手把手教你如何开始DSP编程

    手把手教你如何开始DSP编程。
    发表于 04-09 11:54 12次下载
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>如何开始DSP编程

    手把手教你学LabVIEW视觉设计

    手把手教你学LabVIEW视觉设计手把手教你学LabVIEW视觉设计手把手教你学LabVIEW视
    发表于 03-06 01:41 3107次阅读

    手把手教你开关电源PCB排板

    手把手教你开关电源PCB排板(新型电源技术)-分享下开关电源PCB排板的基本要点及分析,以及例子讲解。绝对的手把手
    发表于 09-18 12:27 57次下载
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>开关电源PCB排板

    手把手教你pcb压合的整个流程,小白也能玩转电路板制作

    手把手教你pcb压合的整个流程,小白也能玩转电路板制作
    的头像 发表于 09-18 10:43 3744次阅读

    手把手教你学FPGA仿真

    电子发烧友网站提供《手把手教你学FPGA仿真.pdf》资料免费下载
    发表于 10-19 09:17 2次下载
    <b class='flag-5'>手把手</b><b class='flag-5'>教你</b>学FPGA仿真