👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料:
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:
  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn

故事的小黄花

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

    StopWatch stopWatch = 
new
 StopWatch();

    stopWatch.start(
"某某方法-耗时统计"
);

    method()

    stopWatch.stop();

    log.info(
"查询耗时分布:{}"
, stopWatch.prettyPrint());

}

这样的代码非常多,侵入性很大,联想到之前学习的Java Agent技术,可以无侵入式地解决这类问题,所以做了一个很小很小的demo
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

Instrumentation

在了解Agent之前需要先看看Instrumentation
JDK从1.5版本开始引入了java.lang.instrument包,该包提供了一些工具帮助开发人员实现字节码增强,Instrumentation接口的常用方法如下
publicinterfaceInstrumentation
{

/**

     * 注册Class文件转换器,转换器用于改变Class文件二进制流的数据

     *

     * 
@param
 transformer          注册的转换器

     * 
@param
 canRetransform       设置是否允许重新转换

     */

voidaddTransformer(ClassFileTransformer transformer, boolean canRetransform)
;


/**

     * 移除一个转换器

     *

     * 
@param
 transformer          需要移除的转换器

     */

booleanremoveTransformer(ClassFileTransformer transformer)
;


/**

     * 在类加载之后,重新转换类,如果重新转换的方法有活跃的栈帧,那些活跃的栈帧继续运行未转换前的方法

     *

     * 
@param
 重新转换的类数组

     */

voidretransformClasses(Class<?>... classes)throws UnmodifiableClassException
;


/**

     * 当前JVM配置是否支持重新转换

     */

booleanisRetransformClassesSupported()
;


/**

     * 获取所有已加载的类

     */

@SuppressWarnings
(
"rawtypes"
)

    Class[] getAllLoadedClasses();

}

publicinterfaceClassFileTransformer
{

// className参数表示当前加载类的类名,classfileBuffer参数是待加载类文件的字节数组
// 调用addTransformer注册ClassFileTransformer以后,后续所有JVM加载类都会被它的transform方法拦截
// 这个方法接收原类文件的字节数组,在这个方法中做类文件改写,最后返回转换过的字节数组,由JVM加载这个修改过的类文件
// 如果transform方法返回null,表示不对此类做处理,如果返回值不为null,JVM会用返回的字节数组替换原来类的字节数组
byte
[] transform(  ClassLoader         loader,

                String              className,

                Class<?>            classBeingRedefined,

                ProtectionDomain    protectionDomain,

byte
[]              classfileBuffer)

throws
 IllegalClassFormatException;

}

Instrumentation有两种使用方式
  1. 在JVM启动的时候添加一个Agent jar包
  2. 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(String agentArgs, Instrumentation instrumentation)
{

}

从字面上理解,就是运行在main()函数之前的类。在Java虚拟机启动时,在执行main()函数之前,会先运行指定类的premain()方法,在premain()方法中对class文件进行修改,它有两个入参
  1. agentArgs:启动参数,在JVM启动时指定
  2. 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(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte
[] classfileBuffer) {

if
 (
"com/example/aop/agent/MyTest"
.equals(className)) {

// 使用ASM框架进行字节码转换
            ClassReader cr = 
new
 ClassReader(classfileBuffer);

            ClassWriter cw = 
new
 ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

            ClassVisitor cv = 
new
 TimeStatisticsVisitor(Opcodes.ASM7, cw);

            cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);

return
 cw.toByteArray();

        }

return
 classfileBuffer;


    }

}

publicclassTimeStatisticsVisitorextendsClassVisitor
{


publicTimeStatisticsVisitor(int api, ClassVisitor classVisitor)
{

super
(Opcodes.ASM7, classVisitor);

    }


@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
{

        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);

if
 (name.equals(
"<init>"
)) {

return
 mv;

        }

returnnew
 TimeStatisticsAdapter(api, mv, access, name, descriptor);

    }

}

publicclassTimeStatisticsAdapterextendsAdviceAdapter
{


protectedTimeStatisticsAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor)
{

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(int opcode)
{

// 退出函数时调用TimeStatistics的静态方法end
super
.onMethodExit(opcode);

super
.visitMethodInsn(Opcodes.INVOKESTATIC, 
"com/example/aop/agent/TimeStatistics"
"end"
"()V"
false
);

    }

}


publicclassTimeStatistics
{

publicstatic
 ThreadLocal<Long> t = 
new
 ThreadLocal<>();


publicstaticvoidstart()
{

        t.set(System.currentTimeMillis());

    }

publicstaticvoidend()
{

long
 time = System.currentTimeMillis() - t.get();

        System.out.println(Thread.currentThread().getStackTrace()[
2
] + 
" spend: "
 + time);

    }

}

publicclassAgentMain
{

// premain()函数中注册MyClassFileTransformer转换器
publicstaticvoidpremain(String agentArgs, Instrumentation instrumentation)
{

        System.out.println(
"premain方法"
);

        instrumentation.addTransformer(
new
 MyClassFileTransformer(), 
true
);

    }

}

<build>

  <plugins>

    <plugin>

      <groupId>org.apache.maven.plugins</groupId>

      <artifactId>maven-assembly-plugin</artifactId>

      <version>
3.1.1
</version>

      <configuration>

        <descriptorRefs>

          <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->

          <descriptorRef>jar-with-dependencies</descriptorRef>

        </descriptorRefs>

        <archive>

          <manifestEntries>

// 指定premain()的所在方法
            <Agent-CLass>com.example.aop.agent.AgentMain</Agent-CLass>

            <Premain-Class>com.example.aop.agent.AgentMain</Premain-Class>

            <Can-Redefine-Classes>
true
</Can-Redefine-Classes>

            <Can-Retransform-Classes>
true
</Can-Retransform-Classes>

          </manifestEntries>

        </archive>

      </configuration>

      <executions>

        <execution>

          <phase>
package
</phase>

          <goals>

            <goal>single</goal>

          </goals>

        </execution>

      </executions>

    </plugin>

    <plugin>

      <groupId>org.apache.maven.plugins</groupId>

      <artifactId>maven-compiler-plugin</artifactId>

      <version>
3.1
</version>

      <configuration>

        <source>${maven.compiler.source}</source>

        <target>${maven.compiler.target}</target>

      </configuration>

    </plugin>

  </plugins>

</build>

使用命令行执行下面的测试类
java -javaagent:/Users/zhangxiaobin/IdeaProjects/aop-demo/target/aop-
0.0.1
-SNAPSHOT-jar-with-dependencies.jar com.example.aop.agent.MyTest

publicclassMyTest
{

publicstaticvoidmain(String[] args)throws InterruptedException 
{

        Thread.sleep(
3000
);

    }

}

计算出了某个方法的耗时
计算出某个方法的耗时

Attach

在上面的例子中,我们只能在JVM启动时指定一个Agent,这种方式局限在main()方法执行前,如果我们想在项目启动后随时随地地修改Class文件,要怎么办呢?这个时候需要借助Java Agent的另外一个方法,该方法的签名如下
publicstaticvoidagentmain(String agentArgs, Instrumentation inst)
{

}

agentmain()的参数与premain()有着同样的含义,但是agentmain()是在Java Agent被Attach到Java虚拟机上时执行的,当Java Agent被attach到Java虚拟机上,Java程序的main()函数一般已经启动,并且程序很可能已经运行了相当长的时间,此时通过Instrumentation.retransformClasses()方法,可以动态转换Class文件并使之生效,下面用一个小例子演示一下这个功能
下面的类启动后,会不断打印出100这个数字,我们通过Attach功能使之打印出50这个数字
publicclassPrintNumTest
{

publicstaticvoidmain(String[] args)throws InterruptedException 
{

while
 (
true
) {

            System.out.println(getNum());

            Thread.sleep(
3000
);

        }

    }

privatestaticintgetNum()
{

return100
;

    }

}

依然是定义一个ClassFileTransformer,使用ASM框架修改getNum()方法
publicclassPrintNumTransformerimplementsClassFileTransformer
{

@Override
publicbyte
[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte
[] classfileBuffer) 
throws
 IllegalClassFormatException {

if
 (
"com/example/aop/agent/PrintNumTest"
.equals(className)) {

            System.out.println(
"asm"
);

            ClassReader cr = 
new
 ClassReader(classfileBuffer);

            ClassWriter cw = 
new
 ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

            ClassVisitor cv = 
new
 TransformPrintNumVisitor(Opcodes.ASM7, cw);

            cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);

return
 cw.toByteArray();

        }

return
 classfileBuffer;

    }

}

publicclassTransformPrintNumVisitorextendsClassVisitor
{

publicTransformPrintNumVisitor(int api, ClassVisitor classVisitor)
{

super
(Opcodes.ASM7, classVisitor);

    }

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
{

        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);

if
 (name.equals(
"getNum"
)) {

returnnew
 TransformPrintNumAdapter(api, mv, access, name, descriptor);

        }

return
 mv;

    }


}


publicclassTransformPrintNumAdapterextendsAdviceAdapter
{


protectedTransformPrintNumAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor)
{

super
(api, methodVisitor, access, name, descriptor);

    }


@Override
protectedvoidonMethodEnter()
{

super
.visitIntInsn(BIPUSH, 
50
);

super
.visitInsn(IRETURN);

    }

}

publicclassPrintNumAgent
{


publicstaticvoidagentmain(String agentArgs, Instrumentation inst)throws UnmodifiableClassException 
{

        System.out.println(
"agentmain"
);

        inst.addTransformer(
new
 PrintNumTransformer(), 
true
);


        Class[] allLoadedClasses = inst.getAllLoadedClasses();

for
 (Class allLoadedClass : allLoadedClasses) {

if
 (allLoadedClass.getSimpleName().equals(
"PrintNumTest"
)) {

                System.out.println(
"Reloading: "
 + allLoadedClass.getName());

                inst.retransformClasses(allLoadedClass);

break
;

            }

        }

    }

}

<build>

  <plugins>

    <plugin>

      <groupId>org.apache.maven.plugins</groupId>

      <artifactId>maven-assembly-plugin</artifactId>

      <version>
3.1.1
</version>

      <configuration>

        <descriptorRefs>

          <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->

          <descriptorRef>jar-with-dependencies</descriptorRef>

        </descriptorRefs>

        <archive>

          <manifestEntries>

// 指定agentmain所在的类
            <Agent-CLass>com.example.aop.agent.PrintNumAgent</Agent-CLass>

            <Premain-Class>com.example.aop.agent.PrintNumAgent</Premain-Class>

            <Can-Redefine-Classes>
true
</Can-Redefine-Classes>

            <Can-Retransform-Classes>
true
</Can-Retransform-Classes>

          </manifestEntries>

        </archive>

      </configuration>

      <executions>

        <execution>

          <phase>
package
</phase>

          <goals>

            <goal>single</goal>

          </goals>

        </execution>

      </executions>

    </plugin>

    <plugin>

      <groupId>org.apache.maven.plugins</groupId>

      <artifactId>maven-compiler-plugin</artifactId>

      <version>
3.1
</version>

      <configuration>

        <source>${maven.compiler.source}</source>

        <target>${maven.compiler.target}</target>

      </configuration>

    </plugin>

  </plugins>

</build>

因为是跨进程通信,Attach的发起端是一个独立的java程序,这个java程序会调用VirtualMachine.attach方法开始合目标JVM进行跨进程通信
publicclassMyAttachMain
{

publicstaticvoidmain(String[] args)throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException 
{

        VirtualMachine virtualMachine = 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.jar com.example.aop.agent.MyAttachMain 
49987
可以清楚地看到打印的数字变成了50
效果

Arthas

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

搭建调试环境

Arthas debug需要借助IDEA的远程debug功能,可以参考 https://github.com/alibaba/arthas/issues/222
先写一个可以循环执行的Demo
publicclassArthasTest
{

publicstaticvoidmain(String[] args)throws InterruptedException 
{

int
 i = 
0
;

while
 (
true
) {

            Thread.sleep(
2000
);

            print(i++);

        }

    }

publicstaticvoidprint(Integer content)
{

        System.out.println(
"Main print: "
 + content);

    }

}

命令行执行改demo
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=
8000
 com.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
.classsuppressHandler
= 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.Return Object returnObject)
{

        System.out.println(
"atExit, returnObject: "
 + returnObject);

    }


@AtExceptionExit
(inline = 
true
, onException = RuntimeException
.
class
)

publicstaticvoidatExceptionExit
(@
Binding
.
ThrowableRuntimeExceptionex
,

                                       @
Binding
.
Field
(
name
"exceptionCount"
int
 exceptionCount) {

        System.out.println(
"atExceptionExit, ex: "
 + ex.getMessage() + 
", field exceptionCount: "
 + exceptionCount);

    }

}

  • 上文说过,bytekit的宗旨是提供简介的API让开发可以轻松地完成字节码增强,从注解名我们就可以知道@AtEnter是在方法进入时插入,@AtExit是在方法退出时插入,@AtExceptionExit时在发生异常退出时插入
  • inline = true表示方法中的代码直接插入增强方法中,inline = false表示是调用这个方法,有点难理解,我们等下看反编译后的代码
  • 配置了 suppress = RuntimeException.classsuppressHandler = PrintExceptionSuppressHandler.class,说明插入的代码会被 try/catch 包围
  • @AtExceptionExit在原方法体范围try-catch指定异常进行处理
这是我们要进行增强的方法
publicclassSample
{

privateint
 exceptionCount = 
0
;

public String hello(String str, boolean exception)
{

if
 (exception) {

            exceptionCount++;

thrownew
 RuntimeException(
"test exception, str: "
 + str);

        }

return"hello "
 + str;

    }

}

publicclassSampleMain
{

publicstaticvoidmain(String[] args)throws Exception 
{

// 解析定义的 Interceptor类 和相关的注解
        DefaultInterceptorClassParser interceptorClassParser = 
new
 DefaultInterceptorClassParser();

        List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor
.class)
;

// 加载字节码
        ClassNode classNode = AsmUtils.loadClass(Sample
.class)
;

// 对加载到的字节码做增强处理
for
 (MethodNode methodNode : classNode.methods) {

if
 (methodNode.name.equals(
"hello"
)) {

                MethodProcessor methodProcessor = 
new
 MethodProcessor(classNode, methodNode);

for
 (InterceptorProcessor interceptor : processors) {

                    interceptor.process(methodProcessor);

                }

            }

        }

// 获取增强后的字节码
byte
[] bytes = AsmUtils.toBytes(classNode);

// 查看反编译结果
        System.out.println(Decompiler.decompile(bytes));

// 修改Sample
        AgentUtils.reTransform(Sample
.classbytes)
;

// 执行sample的方法
try
 {

            Sample sample = 
new
 Sample();

            sample.hello(
"3"
false
);

            sample.hello(
"4"
true
);

        } 
catch
 (Exception e) {

            e.printStackTrace();

        }

    }

}

这是Sample反编译后的结果,代码量剧增
publicclassSample
{

privateint
 exceptionCount = 
0
;


/*

     * WARNING - void declaration

     */

public String hello(String string, boolean bl)
{

try
 {

            String string2;

void
 str;

void
 exception;

try
 {

// @AtEnter 直接调用,inline为false的效果
                SampleInterceptor.atEnter((Object)
this
, Sample
.class, (Object[])newObject[]
{string, 
new
 Boolean(bl)}, (String)
"hello"
, (String)
"(Ljava/lang/String;Z)Ljava/lang/String;"
);

            }

catch
 (RuntimeException runtimeException) {

                Class<Sample> clazz = Sample
.class
;

                RuntimeException runtimeException2 = runtimeException;

                System.out.println(
"exception handler: "
 + clazz);

                runtimeException2.printStackTrace();

            }

if
 (exception != 
false
) {

                ++
this
.exceptionCount;

thrownew
 RuntimeException(
"test exception, str: "
 + (String)str);

            }

            String string3 = string2 = 
"hello "
 + (String)str;

// @AtExit 代码直接插入
            System.out.println(
"atExit, returnObject: "
 + string3);

return
 string2;

        }

catch
 (RuntimeException runtimeException) {

int
 n = 
this
.exceptionCount;

            RuntimeException runtimeException3 = runtimeException;

// @AtExceptionExit 代码直接插入
            System.out.println(
"atExceptionExit, ex: "
 + runtimeException3.getMessage() + 
", field exceptionCount: "
 + n);

throw
 runtimeException;

        }

    }

}

有了这个前置知识,我们来看看trace命令

trace

trace
Arthas命令很多,如果是exit、logout、quit、jobs、fg、bg、kill等简单的命令,就会直接执行,如果是trace这种复杂的命令,会专门用一个类写处理的逻辑,如上图,根据名字就可以猜到这个类是处理什么命令的,这么多类的组织形式是模版模式,入口在com.taobao.arthas.core.shell.command.AnnotatedCommand#process,
publicabstractclassAnnotatedCommand
{

publicabstractvoidprocess(CommandProcess process)
;

}

publicclassTraceCommandextendsEnhancerCommand
{

}

publicabstractclassEnhancerCommandextendsAnnotatedCommand
{

@Override
publicvoidprocess(final CommandProcess process)
{

// ctrl-C support
        process.interruptHandler(
new
 CommandInterruptHandler(process));

// q exit support
        process.stdinHandler(
new
 QExitHandler(process));

// start to enhance
        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)

publicsynchronized EnhancerAffect enhance(final Instrumentation inst)throws UnmodifiableClassException 
{

        ......

try
 {

// 很明显,这里添加了一个文件转换器,注意,此处的转换器为本类
            ArthasBootstrap.getInstance().getTransformerManager().addTransformer(
this
, isTracing);

            ......

        } 
catch
 (Throwable e) {

            logger.error(
"Enhancer error, matchingClasses: {}"
, matchingClasses, e);

            affect.setThrowable(e);

        }

return
 affect;

    }

根据方法名就可以在本类搜索到,具体代码如下
@Override
publicbyte
[] transform(
final
 ClassLoader inClassLoader, String className, Class<?> classBeingRedefined,

                        ProtectionDomain protectionDomain, 
byte
[] classfileBuffer) 
throws
 IllegalClassFormatException {

try
 {

// 检查classloader能否加载到 SpyAPI,如果不能,则放弃增强
try
 {

if
 (inClassLoader != 
null
) {

          inClassLoader.loadClass(SpyAPI
.class.getName())
;

        }

      } 
catch
 (Throwable e) {

        logger.error(
"the classloader can not load SpyAPI, ignore it. classloader: {}, className: {}"
,

                     inClassLoader.getClass().getName(), className, e);

returnnull
;

      }

// 这里要再次过滤一次,为啥?因为在transform的过程中,有可能还会再诞生新的类
// 所以需要将之前需要转换的类集合传递下来,再次进行判断
if
 (matchingClasses != 
null
 && !matchingClasses.contains(classBeingRedefined)) {

returnnull
;

      }

// ClassNode中有各种属性,对应Class文件结构
// keep origin class reader for bytecode optimizations, avoiding JVM metaspace OOM.
      ClassNode classNode = 
new
 ClassNode(Opcodes.ASM9);

      ClassReader classReader = AsmUtils.toClassNode(classfileBuffer, classNode);

// remove JSR https://github.com/alibaba/arthas/issues/1304
      classNode = AsmUtils.removeJSRInstructions(classNode);

// 重要代码,生成增强字节码的拦截器
      DefaultInterceptorClassParser defaultInterceptorClassParser = 
new
 DefaultInterceptorClassParser();

final
 List<InterceptorProcessor> interceptorProcessors = 
new
 ArrayList<InterceptorProcessor>();

      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<MethodNode> matchedMethods = 
new
 ArrayList<MethodNode>();

for
 (MethodNode methodNode : classNode.methods) {

if
 (!isIgnore(methodNode, methodNameMatcher)) {

          matchedMethods.add(methodNode);

        }

      }

// https://github.com/alibaba/arthas/issues/1690
if
 (AsmUtils.isEnhancerByCGLIB(className)) {

for
 (MethodNode methodNode : matchedMethods) {

if
 (AsmUtils.isConstructor(methodNode)) {

            AsmUtils.fixConstructorExceptionTable(methodNode);

          }

        }

      }

      .......

for
 (MethodNode methodNode : matchedMethods) {

if
 (AsmUtils.isNative(methodNode)) {

          logger.info(
"ignore native method: {}"
,

                      AsmUtils.methodDeclaration(Type.getObjectType(classNode.name), methodNode));

continue
;

        }

// 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener
if
(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI
.class), "atBeforeInvoke")) 
{

for
 (AbstractInsnNode insnNode = methodNode.instructions.getFirst(); insnNode != 
null
; insnNode = insnNode

               .getNext()) {

if
 (insnNode 
instanceof
 MethodInsnNode) {

final
 MethodInsnNode methodInsnNode = (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
 {

// 重点代码,增强动作就是在这里完成的
          MethodProcessor methodProcessor = 
new
 MethodProcessor(classNode, methodNode, groupLocationFilter);

for
 (InterceptorProcessor interceptor : interceptorProcessors) {

try
 {

              List<Location> locations = interceptor.process(methodProcessor);

for
 (Location location : locations) {

if
 (location 
instanceof
 MethodInsnNodeWare) {

                  MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location;

                  MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode();

                  AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className,

                                                                    methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener);

                }

              }

            } 
catch
 (Throwable e) {

              logger.error(
"enhancer error, 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 的major version是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);

    }

returnnull
;

}

这段代码很长,其实主要逻辑就两个
  • 解析Interceptor Class的@AtXxx,@Binding等注解,生成InterceptorProcessor对象集合
  • 遍历InterceptorProcessor集合,修改原方法的字节码
整体的流程如下图
整体的流程如图
那这些拦截器长什么样子呢?我们随便找一个例子来看看
publicstaticclassSpyInterceptor1
{

@AtEnter
(inline = 
true
)

publicstaticvoidatEnter
(@Binding.This Object target, @Binding.Class Class<?> clazz,

                               @Binding.MethodInfo String methodInfo, @Binding.Args Object[] args)
{

      SpyAPI.atEnter(clazz, methodInfo, target, args);

    }

}

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

参考资料

  • https://developer.aliyun.com/article/768074
  • https://arthas.aliyun.com/doc/trace.html#注意事项
  • https://blog.csdn.net/tianjindong0804/article/details/128423819

欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,长按”或“扫描”下方二维码噢
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
继续阅读
阅读原文