1 Star 0 Fork 0

cxylk / Java-Notes

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
字节码编程.md 29.68 KB
一键复制 编辑 原始数据 按行查看 历史
cxylk 提交于 2022-09-06 20:29 . :construction:asm工具

javaagent

提供一个修改类的入口。

启动方式

加载启动(premain)

运行时jar包 javaagent jar包
启动类 Main-Class Premain-Class
启动方法 main premain
启动方式 java -jar xxx.jar -javaagent:xxx.jar

流程:

1、编写好启动类和方法

public class MyAgent {
    //加载时启动
    public static void premain(String args, Instrumentation instrumentation){
        System.out.println("premain");
    }
}

2、编写配置文件,主要是premain方法所在的类,使其能够找到。有两种配置方式

  • 在resource目录下新建META-INF目录,在下面新建MANIFEST.MF文件,一般配置内容如下

    Manifest-Version: 1.0
    Premain-Class: com.cxylk.MyAgent #premain方法所在的类
    Can-Redefine-Classes: true

    然后在pom文件中添加MF文件路径的配置

    <properties>
    		...
            <!-- 自定义MANIFEST.MF -->
            <maven.configuration.manifestFile>src/main/resources/META-INF/MANIFEST.MF</maven.configuration.manifestFile>
        </properties>
  • 将配置内容直接在pom文件中配置,如下:

        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Project-name>${project.name}</Project-name>
                                <Project-version>${project.version}</Project-version>
                                <!-- 不需要javassist可以去掉下面这行-->
                                <Boot-Class-Path>javassist-3.18.1-GA.jar</Boot-Class-Path>
                                <Premain-Class>com.cxylk.agent.AgentMain</Premain-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            </manifestEntries>
                        </archive>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </build>

3、调用main方法配置 Run/Debug configurations,添加VM 参数-javaagent:打好的jar包=参数名(premian方法的第一个参数)

附着启动(agentmain)

如果想要在应⽤运⾏之后去监听它,⽽⼜不去重启它,就可以采⽤另⼀种⽅式附着启动。其相关属性通过以下表来⽐对

运行时jar包 javaagent jar包
启动类 Main-Class Agent-Class
启动方法 main agentmain
启动方式 java -jar xxx.jar tools工具附着

1、同样编写好启动类和方法(第二个,和第一个没关系):

public class MyAgent {
    //加载时启动
    public static void premain(String args, Instrumentation instrumentation){
        System.out.println("premain");
    }

    //运行时启动
    public static void agentmain(String args,Instrumentation instrumentation){
        System.out.println("agentmain");
    }
}

2、编写配置文件,同样有两种配置方法,这里选用第二种:

                    <archive>
                        <manifestEntries>
                            <Project-name>${project.name}</Project-name>
                            <Project-version>${project.version}</Project-version>
                            <Premain-Class>com.cxylk.agent.MyAgent</Premain-Class>
                            //agentmain方法所在的类
                            <Agent-Class>com.cxylk.agent.MyAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>

3、编写加载器

这是和上面那种方式最大的区别,因为附着启动,它是在程序运行中监控类,所以不能再像上面那种方式在VM参数上加上jar包就行了,而是要通过jvm/lib/tools.jar中的API注入至目标应用:

public class AttachStart {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        //获取jvm进程列表
        List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
        for (int i = 0; i < virtualMachineDescriptors.size(); i++) {
            System.out.println(String.format("[%s] %s",i,virtualMachineDescriptors.get(i).displayName()));
        }
        System.out.println("请输入指定要attach的进程");
        //选择JVM进程
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String readLine = bufferedReader.readLine();
        int i = Integer.parseInt(readLine);
        //附着agent
        VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptors.get(i));
        //传入agentmain方法所在类的jar包
        virtualMachine.loadAgent("F:\\github\\treasure-map\\javaagent\\target\\javaagent-1.0-SNAPSHOT.jar","hello");
        virtualMachine.detach();
        System.out.println("加载成功");
    }
}

然后运行该方法,需要注意此时我们要监控的目标方法所在的进程要是运行的,不然就获取到的jvm进程列表中就没有目标方法所在的进程。比如我们的目标方法可以这样写:

public class MyApp {
    public static void main(String[] args) throws IOException {
        System.out.println("main");
        System.in.read();
    }
}

此时就可以选择进程监控了:

...
[2] com.cxylk.agent.MyApp
[3] com.cxylk.agent.AttachStart
[4] 
[5] org.jetbrains.idea.maven.server.RemoteMavenServer36
请输入指定要attach的进程
2
加载成功

然后去MyApp的控制台就可以看到结果,这里是看不到结果的

main
agentmain

下面这张图就是上面的流程

两种启动方式的区别

区别在于加载时机,premain⽅式启动可以在类加载前进⾏启动,进⽽可以完整的修改类。agentmain是在系统运⾏中启动,其只能修改类的部分逻辑,监控上是有限制的。 其它的API应⽤都是⼀样的。

核心应用

类加载拦截

addTransformer 添加类加载拦截器,只能拦截未装载过的类(执行premain方法时没有被装载过的类),可重定义加载的类。

类重新加载

retransformClasses 重新触发类的加载,类加载后⽆法被addTransformer 拦截,该⽅法可重 新触发拦截,进⾏进⼆次加载类。注意加载的类是有限制的,仅可对运⾏指令码进⾏修改: 不可修改类结构如继承、接⼝、类符、变更属性、变更⽅法等。可以新增private static/final 的 方法; 必须添加 Can-Retransform-Classes=true 该⽅法执⾏才有效,且addTransformer⽅ 法的canRetransform参数也为true。

类重定义

redefineClasses 重新定义类,不能添加新方法 ,必须开启相关参数,Can-Redefine-Classes为true。该⽅法会触发retransformClasses 类似的逻辑

javaagent+javassist

javaagent只是提供了一个可以修改类的入口,具体还是要通过javassist或者asm来实现,这里以javassist实现对原有方法的修改:

    public static void premain(String args, Instrumentation instrumentation){
        //拦截所有未加载的类
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                //只需要拦截HelloWorld类即可
                if(!"com/cxylk/agent/HelloWorld".equals(className)){
                    return null;
                }

                //javassist
                try {
                    ClassPool classPool = new ClassPool();
                    classPool.appendSystemPath();
                    CtClass ctClass = classPool.get("com.cxylk.agent.HelloWorld");
                    CtMethod method = ctClass.getDeclaredMethod("hello");
                    method.insertBefore("System.out.println(\"插入前置逻辑\");");
                    return ctClass.toBytecode();
                } catch (NotFoundException | CannotCompileException | IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }

上面所做的事情就是在HelloWorld类中的hello方法中添加一行代码,结果如下:

main
插入前置逻辑
hello

但如果在执行addTransformer方法之前,在它前面加上了这一行代码:

HelloWorld helloWorld=new HelloWorld();

那么就不会走下面的逻辑,因为此时HelloWorld这个类已经加载过了,但是我就是想让它重新加载怎么办呢?用我们上面说过的retransformClasses 方法,并且不要忘记设置那两个true:

instrumentation.retransformClasses(HelloWorld.class);

这样,就能输出和上面一样的结果了。

如果继续在下面加入一段重定义的代码:

        // 重新定义
        try {
            ClassPool pool=new ClassPool();
            pool.appendSystemPath();
            CtClass ctClass = pool.get("com.cxylk.agent.HelloWorld");
            CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
            ctMethod.insertAfter("System.out.println(\"插入后置逻辑\");");
            instrumentation.redefineClasses(new ClassDefinition(HelloWorld.class,ctClass.toBytecode()));
        } catch (NotFoundException | CannotCompileException | IOException | UnmodifiableClassException | ClassNotFoundException e) {
            e.printStackTrace();
        }

那么“插入后置逻辑”这句话能加入到原方法吗?答案是不能,因为上面说过,重定义会执行和重新加载相似的逻辑,也就是会去再执行上面的拦截逻辑,而在拦截逻辑里面我们又对这个类重新进行了修改,也就是说不是在原有的基础上进行修改了,那怎么才能在原有的基础上进行修改呢?我们在拦截的逻辑中加上这段代码:

                    //必须要在第二行代码之前添加类路径
                    classPool.appendClassPath(new ByteArrayClassPath("com.cxylk.agent.HelloWorld",classfileBuffer));
                    classPool.appendSystemPath();

这样的话,原来方法中就能插入“插入后置逻辑”这句话了。其实就是添加类路径时,这个路径是根据HelloWorld和它的字节数组得来的,而这个字节数组就是在进行类重新定义时传来的字节数组,所以才能在原有的基础上操作。但是这种方式并不推荐,最好还是使用拦截器或者重新加载的方式

Javassist

提供Api以java的方式增强字节码

首先需要先获取类池,然后往类池中装载类,有两种写法:

1、全写:

//创建一个类池,现在是空的,必须要给定一个加载类的路径
ClassPool classPool=new ClassPool();
装载当前类的classloader下的类到类池中将系统搜索路径加到搜索路径中
classPool.appendSystemPath();

2、获取默认的类池

ClassPool classPool=ClassPool.getDefault();

它会自动调用classPool.appendSystemPath();

除了appendSystemPath这种方式,还可以通过jar、class目录、classloader、jar目录、字节数字byte[]来加载一个类

3、需要注意的是,如果要加载的类不在当前类的classloader下,那么就要使用下面这种写法:

ClassPool.insertClassPath(classpath)

classpath就是加要加载的类所在的路径。

比如现在(maven项目)在test包下有一个javassistTest类,这个类中要去操作Java包下UserService类中的某个方法,当javassistTest被解析成字节码时,它的class文件是在target目录中的test-classes下,那么现在要去加载UserService类,就要插入UserService所在类的路径(classes包下),否则就会报无法找到的异常。

无论是insertBefore还是insertAfter方法都是以代码块的形式插入,比如insertBefore插入long begin=System.currentTimeMillis();,insertAfter插入long end=System.currentTimeMillis()-begin这样是不行的,因为begin的作用域不在insertAfter的代码块内

#insertBefore在代码之前插入
{
    long begin=System.currentTimeMillis();
}
#insertAfter在代码之后插入
{
    long end=System.currentTimeMillis()-begin;
}

这时候可以拷贝原来的方法然后重新构建一个新的方法,比如这样:

//拷贝原方法重新生成一个新方法,解决原方法插入代码时因为都是以代码块插入而造成局部变量访问不到的问题
        CtMethod newMethod = CtNewMethod.copy(sayHello, ctClass, null);
        //改变原来方法的名称
        sayHello.setName(sayHello.getName()+"$agent");

        newMethod.setBody("{long begin=System.currentTimeMillis();\n" +
                "        sayHi$agent($$);\n" +
                "        long end=System.currentTimeMillis();\n" +
                "        System.out.println(end-begin);"+
                "        Object a=\"lk\";"+
                "        return ($r)$3;"+
                "        }"
        );
        //加入该方法
        ctClass.addMethod(newMethod);
        //把修改之后的类 装载到JVM
        ctClass.toClass();

需要注意toClass这个方法,它用来将类加载到classLoader,并且这个类要没有加载过才可以,如果一个类已经被加载过,那么再调用这个方法就会报错,而且一旦调用该方法,就不能再进行进一步的修改

如何生成类的字节码文件呢?调用toBytecode方法即可,同样的,一旦调用该方法,就不能再进行进一步的修改

特殊语法这里就不详细介绍了,网上应该有很多文章。

当然,除了可以往方法中添加代码,也可以将代码中的代码删除,只需要调用CtMethod类中的instrument(ExprEditor)方法,重写ExprEditor类中的edit方法即可

总结

  1. 类池:ClassPool⽤于解析类,并缓存已经解析的类,⼀直使⽤⼀个 ClassPool 可能导致内 存溢出。
  2. 类路径(ClassPath):解析类时的加载路径,⽀持jar包、class⽬录、JAVA⽬录,已加载 的ClassLoade
  3. 转换类(CtClass):读取Class 字节码,并解析成CtClass 对象
  4. 代码插⼊(CtMethod): 在⽅法的前后可插⼊代码块,须符合JAVA语法规则。

ASM

ASM和Javasisit一样,是一款动态修改字节码的工具,不过它更强大,更灵活,性能更好,当然也更难,因为需要直接跟字节码打交道,所以要对字节码特别的熟悉。在OpCodes这个类中定义了JVM中的所有操作码和对应的助记符,我们可以直接拿来用。

设计模式

ASM使用了访问者+责任链的设计模式,我们来看一下它的基本模式:

理解这张图是我们使用ASM的一个基础,Reader用于读取指定的类文件,在ASM中就是ClassReader,只有一个,然后我们就需要指定访问者来访问它,这个访问者有很多个,它们都需要继承ClassVisitor类,也就是说我们可以根据需要灵活配置多个visitor,但是我们要为visitor指定下一个visitor,因为它们是一个责任链,上一个visitor处理完之后交给链条中的下一个visitor处理。最后是Writer,它是责任链中最后一个visitor,用于将最终被修改的class文件写入。

由于可以配置多个visitor,所以我们可以随意组合成复杂模式:

这里有2个特殊的visitor需要说一下,就是ClassVisitorMethodVisitor,分别表示类的访问者和方法的访问者(也就是修改类和方法的入口),不管是类的访问者还是方法的访问者,它们都是具有严格的执行顺序的,比如ClassVisitor中以visit方法表示开始,visitEnd表示结束,而在MethodVisitor中,以visitorCode表示开始,visitEnd表示结束。这个顺序在类的注释上写得很清楚,我们使用的时候需要严格遵守这个顺序,不然会出错。

使用案例

我们通过下面几个例子来了解ASM的使用:

一、打印类的基本结构

详见treasure-map项目下的asm-coverage模块下的ClassVisitorTest

二、修改类名、修改方法名

代码如下:

    /**
     * 对一个类进行增删改
     * @throws IOException
     */
    @Test
    public void writeTest2() throws IOException {
        //指定要读取的类
        ClassReader reader=new ClassReader("com.cxylk.Hello");
        //ClassWrite也是一个访问者,它是责任链中的最后一个,用于复制读取到的类
        //flags传的参数表示自动计算最大堆栈大小和最大局部变量大小,栈帧映射
        ClassWriter writerVisitor=new ClassWriter(reader,ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS);
        //新增一个visitor,用于增删改
        //第2个参数表示下一个访问者是谁
        ClassVisitor updateVisitor=new ClassVisitor(ASM5,writerVisitor) {
            /**
             * 访问类的头部,也就是说该方法是开始的地方
             */
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, access, name, signature, superName, interfaces);
            }

            /**
             * 访问类的头部,也就是结束访问
             * 注意,这里实现方法的顺序不是访问的顺序,也就是说这里看visitEnd在visitMethod前面,
             * 但是执行结果是visitEnd在最后面
             * 添加一个字段或方法可以在开始的地方添加,也可以在结束的地方添加,比如我们在这里添加一个方法
             */
            @Override
            public void visitEnd() {
                //添加方法
                //一定要在调用父类方法之前添加,并且最后一定要调用visitEnd通知访问者已经访问完了
                this.visitMethod(ACC_PUBLIC,"isSexy","(LJava/lang/Object;)I",null,null).visitEnd();
                super.visitEnd();
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                //删除方法,直接返回null
                if("hello".equals(name)){
                    return null;
                }
                //修改指定方法的名称
                if("hi".equals(name)){
                    return super.visitMethod(access, name+"$agent", desc, signature, exceptions);
                }else {
                    return super.visitMethod(access, name, desc, signature, exceptions);
                }
            }
        };
        //一定要加入访问者,不然访问不了
        reader.accept(updateVisitor,0);
        //这时候得到的就是和Hello一模一样的class 字节数组
        byte[] bytes = writerVisitor.toByteArray();
        Files.write(new File(System.getProperty("user.dir")+"/target/Hello$agent.class").toPath(),bytes);
    }

三、添加类,添加字段,添加方法

代码如下:

    /**
     * 新增一个类
     */
    @Test
    public void newTest() throws IOException {
        //构造函数不指定classReader
        ClassWriter writerVisitor=new ClassWriter(ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS);
        //设置类信息
        writerVisitor.visit(V1_8, ACC_PUBLIC | ACC_ABSTRACT |ACC_INTERFACE,
                "com/cxylk/NewClass", null, "java/lang/Object",
                new String[] { "java/lang/Runnable" });
        //添加字段,一定要跟visitEnd表示结束
        writerVisitor.visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "name", "Ljava/lang/String;",
                null,"lk")
                .visitEnd();
        //添加方法
        writerVisitor.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, "isSexy",
                "(Ljava/lang/Object;)I", null, null)
                .visitEnd();
        writerVisitor.visitEnd();
        byte[] bytes = writerVisitor.toByteArray();
        //写入文件
        Files.write(new File(System.getProperty("user.dir")+"/target/NewClass.class").toPath(),bytes);
    }

工具类

ASM中有几个很重要的工具类,分别是

  • TraceClassVisitor:打印类的基本结构与指令码

  • CheckClassAdapter 验证语法

    如果不进行语法验证,指令错了也会生成,而不会提示错误

  • ASMifier 反向生成类创建的 ASM代码(经常会用到)

    当我们需要新增一个方法或者修改一个方法的时候,所涉及到的指令是很多的,而且也很容易出错,所以最好的办法就是将所需要新增或修改的代码先写好,然后通过这个工具生成ASM代码:

        @Test
        public void traceTest() throws IOException {
            ClassReader reader=new ClassReader("com.cxylk.Hello");
            ASMifier asMifier=new ASMifier();
    
            TraceClassVisitor traceClassVisitor=new TraceClassVisitor(null,asMifier,new PrintWriter(System.out));
            reader.accept(traceClassVisitor,0);
        }

这三个类的使用例子详见ClassVisitorTest类。

我们以添加方法和修改方法为例说明下如何操作,因为这两个是最难的,上面我们虽然讲过如何添加方法和修改方法,但是都是简单的添加一个方法名称或修改方法名称,没有涉及到方法体。

添加方法

假如我们需要在Hello中添加如下方法:

    public int isSexy(Object a,int b,boolean c){
        System.out.println("hello");
        return 1;
    }

那么我们可以通过ASMifer生成ASM代码,假如我们需要在类的末尾添加这个方法,那么将生成的ASM代码拷贝进去:

    @Test
    public void newTest() throws IOException {
        ClassReader reader=new ClassReader("com.cxylk.Hello");
        ClassWriter writer=new ClassWriter(ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS);
        //添加语法校验适配器
        CheckClassAdapter checkClassAdapter=new CheckClassAdapter(writer);
        //添加classVisitor用于修改类
        ClassVisitor updateVisitor=new ClassVisitor(ASM5,checkClassAdapter) {
            /**
             * 在类的结束位置添加方法
             * 添加方法的常用做法:将需要添加的方法用正常Java代码写出来,然后在用ASMifer生成ASM代码
             */
            @Override
            public void visitEnd() {
                {
                    //调用visitMethod添加一个方法,指定名称等属性
                    //其实就是调用下一个visitor的visitMethod,因为没有这个方法,所以最后会走到
                		//责任链最后的ClassWriter的visitMethod方法,由它创建一个方法出来
                    MethodVisitor mv = this.visitMethod(ACC_PUBLIC, "isSexy", "(Ljava/lang/Object;IZ)I", null, null);
                    //表示方法的开始
                    mv.visitCode();
                    //label主要用于分支流程语句跳转
                    Label l0 = new Label();
                    mv.visitLabel(l0);
                    //行号
                    mv.visitLineNumber(33, l0);
                    //调用静态方法out
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    //将hello从常量池推送至栈顶
                    mv.visitLdcInsn("hello");
                    //调用println方法
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    Label l1 = new Label();
                    mv.visitLabel(l1);
                    mv.visitLineNumber(34, l1);
                    //将int类型1推送至栈顶
                    mv.visitInsn(ICONST_1);
                    //添加return指令返回
                    mv.visitInsn(IRETURN);
                    Label l2 = new Label();
                    mv.visitLabel(l2);
                    //访问局部变量表,因为是实例方法,所以槽位为0的地方是this
                    mv.visitLocalVariable("this", "Lcom/cxylk/Hello;", null, l0, l2, 0);
                    mv.visitLocalVariable("a", "Ljava/lang/Object;", null, l0, l2, 1);
                    mv.visitLocalVariable("b", "I", null, l0, l2, 2);
                    mv.visitLocalVariable("c", "Z", null, l0, l2, 3);
                    //操作数栈需要操作2次,局部变量表需要4个槽位,它们的大小都是由最大值决定
                    mv.visitMaxs(2, 4);
                    //该方法访问结束
                    mv.visitEnd();
                }
                //调用父类的visitEnd其实就是调用链条中下一个visitor,因为我们
                //在构造函数中指定了下一个visitor
                super.visitEnd();
            }
        };

        reader.accept(updateVisitor,0);
        byte[] bytes = writer.toByteArray();
        Files.write(new File(System.getProperty("user.dir")+"/target/Hello.class").toPath(),bytes);
    }

查看Hello.class就可以看到方法被成功添加。

修改方法

比如在方法的开头添加开始时间,方法结束计算耗时:

//开始
long begin = System.currentTimeMillis();

//结束
System.out.println(System.currentTimeMillis() - begin);

这个时候就需要重新visitMethod方法

						//修改方法
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
                if ("hi".equals(name)) {
                    //LocalVariablesSorter自动计算局部变量表的位置
                    return new LocalVariablesSorter(ASM5, access, desc, methodVisitor) {
                        int time;
                        @Override
                        public void visitCode() {// 方法开始 加入begin
                            super.visitCode();
                            visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                            //需要new一个局部变量,传入变量类型
                            time = newLocal(Type.LONG_TYPE);
                            //放入局部变量表
                            visitVarInsn(LSTORE, time);
                        }

                        @Override
                        public void visitInsn(int opcode) {
                            //在return之前添加
                            if (opcode >= IRETURN && opcode <= RETURN) {
                                visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                                visitVarInsn(LLOAD, time);
                                visitInsn(LSUB);
                                visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
                            }
                            super.visitInsn(opcode);
                        }

                        @Override
                        public void visitMaxs(int maxStack, int maxLocals) {
                            super.visitMaxs(4, 4);
                        }

                        @Override
                        public void visitEnd() {

                            super.visitEnd();
                        }

                    };
                }
                return methodVisitor;
            }

这里需要注意,计算耗时其实加的是这几行代码:

                                visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                                visitVarInsn(LLOAD, time);
                                visitInsn(LSUB);
                                visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);

但是这几行代码应该加在哪呢?前面说过visitorEnd表示访问方法结束,所以把代码加在结束的地方也没什么问题,但是我们也说过,不管是ClassVisitor还是MethodVisitor,它们的访问都是具有严格的顺序的,而在MethodVisitor中,在调用visitorEnd时,visitorMax一定已经被调用了,但是visitorxxxInsn这些方法执行完后又会执行visitorMax,所以这时就会报错,说visitorMax已经被调用过了,所以应该拦截visitInsn方法,在return指令执行之前插入上面的代码。

另外注意下LocalVariablesSorter这个visitor,当我们需要在方法插入一个局部变量的时候,我们是需要计算它应该放在局部变量表的哪个槽位,如果是我们手动计算,很容易出错,这个时候就可以用这个visitor来自动计算。

1
https://gitee.com/cxylk/Java-Notes.git
git@gitee.com:cxylk/Java-Notes.git
cxylk
Java-Notes
Java-Notes
main

搜索帮助