前言
在前面博客中,我们通过写好的 Java 文件再编译成 class 文件来替换目标类,这样存在着一定的局限性。而使用 asm 可以直接对 class 文件进行读写,本文将演示如何通过ASM Bytecode Outline 插件对 class 文件进行加强。以下内容转载自:手摸手增加字节码往方法体内插代码
手摸手增加字节码往方法体内插代码
话不多说,先看本次想实现怎样的效果:
1 | public static class Bazhang { |
这是一个自定义的类,里面有三个方法,我需要在不改变原有写好的代码的基础上,往newFunc(String str)这个方法内收尾增加两个方法,打印输入start和end,也就是如下:
1 | public static void newFunc(String str) { |
那么我们直接操作字节码,往方法体内首尾增加相应字节码就好了。
这里先安利一个IntelliJ的插件,叫做ASM Bytecode Outline,他可以直接显示java代码对应的字节码和ASM相应的操作代码,这个插件一定程度上帮助我们写接下来的代码。
下面进入正题,手摸手开始:
自定义一个ClassVisitor
1 | static class TestClassVisitor extends ClassVisitor { |
确保只有newFunc方法才会走我们的套路。
自定义TestMethodVisitor
1 | static class TestMethodVisitor extends MethodVisitor { |
我们注释了两个方法,一个是visitCode(),一个是visitInsn(int opcode),这两个方法一个为我们接下来插入start,一个插入end。
使用ASM Bytecode Outline插件做简单分析
这一步并不是必须的,如果你对字节码足够的熟练,完全可以随便撸。
先随便写个类,然后实现我们最后需要变成的newFunc(String str)方法,然后用插件查看ASMifield,可以得到如下片段:
1 | // 1 |
我把中间很多代码给删掉了,这样结构就很清楚了,可以看到我留下了四个部分:
注释1:这是我们要插入的System.out.println(“========start=========”);转成相对应的ASM提供的方法,其中visitLdcInsn可以在JVM指令表中查到Ldc表示将int, float或String型常量值从常量池中推送至栈顶,那么其实ASM提供了方法让我们继续通过java代码转成相对应的字节码,那么注释1中的三行代码所对应的字节码就是:
1 | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; |
注释2:这是原方法内的代码System.out.println(str);,表明我们注释1确实是插在了newFunc方法体的最上方,其中mv.visitVarInsn(ALOAD, 0);的ALOAD对应JVM指令的意思是将指定的引用类型本地变量推送至栈顶,因为这个String是参数传过来的。
注释3:显而易见,转换前的java代码就是我们的System.out.println(“========end=========”);,这里不再赘述。
注释4:RETURN对应着从当前方法返回void
这样一来,ASM Bytecode Outline插件帮我们直接生成了相对应的ASM代码,那么我们接下来粘贴复制就行了。
插头
先从简单的开始,插头部。我们在之前的自定义TestMethodVisitor中已经复写了visitCode方法,那么我们就在代码注释的地方插入ASM代码:
1 |
|
插尾
这个比插头复杂点,但是也很简单,visitInsn方法会在每个指令被执行时都会调用,所以我们需要判断指令是否到了RETURN即可,在RETURN前插入我们的代码:
1 | if (opcode == Opcodes.RETURN) { |
校验
这样一来,我们的头尾的插好了,校验一番:
1 | public static void main(String[] args) throws Exception { |
可以看到如下结果:
1 | import java.util.List; |
看来导出的.class是没问题了,那么利用反射来执行一下我们的修改类:
1 | Test loader = new Test(); |
最后控制台打印出来的结果是:
1 | ========start========= |
尾语
这样一来我们的目的都达到了,之后便可以更加进一步做点有意思的事,比如统计方法耗时。
整的来说,修改字节码达到本次想要的效果是个很cool的方式,很多时候我们是可以通过hook或者动态代理来做一些类似本文的操作,那么就要结合实际情况进行选择了。