前言
使用ASM改写字节码实现Aop,是最快的Aop实现方式。
我猜你肯定懂AOP
凡是学习Spring框架,必然会深入了解AOP的原理以及实现。这里做下简单总结
Spring默认采取的是动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。这里的CGlib机制的底层就是基于ASM来实现的。
但是Spring的AOP有一定的缺点,它只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。并且同类中的互相调用方法将不会使用代理类。
但你有没有尝试过,不依赖Spring框架,自己来实现AOP编程呢?
那么接下来,我将自己最近学习以及了解的ASM字节码操纵框架来实现AOP编程。
当然了,如果你对JVM还没有较深入的了解或认识,这篇文章读起来会比较吃力。
如果你想快速了解认识ASM字节码框架,首先必须要了解熟悉JVM中类文件结构部分。
好了,咖啡宝贝(CAFEBABE),我要开始发车了,系好安全带哦!
一、ASM是什么?
- ASM 是一个 Java 字节码操纵和分析框架。它可以直接以二进制形式动态地生成 stub 类或其他代理类,或者在装载时动态地修改类。
- ASM 提供类似于 BCEL 和 SERP 之类的工具包的功能,但是被设计得更小巧、更快速,这使它适用于实时代码插装。
- ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。
- ASM提供与其他Java字节码框架类似的功能,但专注于性能。 因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
接下来简单介绍ASM编程模型
- Core API : 提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存,但这种编程方式难度较大(咱们接下来演示就采用该种模型)
- Tree API:提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方法需要更多的内存,这种编程方式较简单
进一步介绍Core API:
- Core API中操纵字节码的功能是基于ClassVisitor接口。而A这个接口中的每个方法对应了class文件中的每一项
- 当然ASM提供了三个基于ClassVisitor接口的类来实现class文件的生成和转换。
- ClassReader : ClassReader解析一个类的class字节码
- ClassAdapter : ClassAdapter是ClassVisitor 的实现类,实现要变化的功能。或者说切入的功能代码
- ClassWriter : ClassWriter 也是ClassVisitor的实现类,可以用来输出变化后的字节码,给予JVM运行处理
- ASM给我们提供了ASMifier工具来帮助开发,可使用ASMifier工具生成ASM结构来对比
- 如果没有ASMifier工具,自己去构建是非常吃力的,接下来代码实现的时候,我将使用该工具进行演示
- 好了,直接进入正题了。咖啡BABE,准备好了没?
二、代码实现
1.环境设置
这里为什么叫环境设置呢?随便称呼的,哈哈哈哈
因为ASMifier的使用需要借助里面的org.objectweb.asm.util.ASMifier辅助我们操作
所以起初大家可以安装一个插件
ASM Bytecode Outline 0.3.5
重启Idea之后
这就说明咱们插件安装成功了,这里也就可以直接明了的看到字节码指令了
2.分析对比
我给定的测试类文件代码
package com.guanbo.asm;
public class Test01 {
public void test() {
System.out.println("location:com.guanbo.asm.Test01");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
原字节码方法区:
常量池:
借助ASMified生成的:
在ASMified中可以清晰的发现,众多JVM虚拟机的指令集,如果还不清楚什么意思,建议复习一下哦
并且通过ASMified均是通过visit去访问的,具体的访问细节咱们晚点说
3.增加需求(输出运行时间)
假如我们不考虑修改字节码文件的方式
直接在改Test01.java类中操作
那么我们必然要加入以上两条代码输出时间差
在测试类中实现的代码(需要增加的方法):
package com.guanbo.asm;
public class Test01 {
public void test() {
Long a1 = System.currentTimeMillis();
System.out.println("location:com.guanbo.asm.Test01");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Long a2 = System.currentTimeMillis();
System.out.println("invoke method total time ==" + (a2 - a1));
}
}
对比
这里我们对比通过ASMified自动生成对应的ASM代码来分析
我们只需对比分析test()方法内的字节码即可
增加需求之前的ASMified自动解析生成的ASM代码:
增加需求之后
补充:其实在这里就可以看到,程序在编译期时,JDK已经做了相应的JVM优化
这里String,StringBuffer,StringBuilder的区别以及性能的分析,以及在多线程中的适用性、安全性是目前我了解到的面试题或者笔试题中会经常出现的,不晓得咖啡BABE你是不是已经了如指掌了呢?
另外再配上增加需求之后对应的字节码文件:
我们可以先来看一下字节码文件中的本地变量表
test()方法
对比之后,不考虑变量的情况下
那么
进而输出了不一样的字节码的文件
接下来咱们开始创建自己的Visitor
这里会涉及到asm包下的org.objectweb.asm.Opcodes类
在该类中基本涵盖了各种JVM虚拟机的字节码指令以及操作码常量,后面的所有方法的执行,均需要调用该类中的字节码指令属性咱们接下来所涉及的只重写vistMethond以及visit方法(其他方法均类似,可自由测试,欢迎大家跟我一起探讨交流)
植入代码(重写ClassVisitor)
代码如下(示例):
package com.guanbo.asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
//这里需要过滤掉<init>JVM执行时初始化的方法
if (!"<init>".equals(name) && mv != null) {
//这里便开始植入所需要的功能需求代码
mv = new MyMethodVistor(mv);
}
return mv;
}
}
class MyMethodVistor extends MethodVisitor {
public MyMethodVistor(MethodVisitor methodVisitor) {
super(Opcodes.ASM7, methodVisitor);
}
@Override
public void visitCode() {
//导入需要植入的指令
mv.visitCode();
//
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 1);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLineNumber(7, l4);
}
@Override
public void visitInsn(int opcode) {
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 3);
Label l7 = new Label();
mv.visitLabel(l7);
mv.visitLineNumber(14, l7);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("invoke method total time ==");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.LLOAD, 3);
mv.visitVarInsn(Opcodes.LLOAD, 1);
mv.visitInsn(Opcodes.LSUB);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l8 = new Label();
mv.visitLabel(l8);
mv.visitLineNumber(16, l8);
}
mv.visitInsn(opcode);
}
}
到目前为止,我们已经将功能写入了,但是我们怎么去用呢?
这里我们需要一个Generator把这些功能作用到我们类上并且输出出去!
构建Generator
到这了就只需要关注咱们之前提到的三部曲了
- ClassReader : ClassReader解析一个类的class字节码
- ClassAdapter : ClassAdapter是ClassVisitor 的实现类,实现要变化的功能。或者说切入的功能代码
- ClassWriter : ClassWriter 也是ClassVisitor的实现类,可以用来输出变化后的字节码,给予JVM运行处理
关于:ClassWriter.COMPUTE_MAXS):表示交给ASM 自动帮你计算局部变量表和操作数栈的大小
代码如下(示例):
package com.guanbo.asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class Generator {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("com/guanbo/asm/Test01");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new MyClassVisitor(cw);
cr.accept(cv, ClassReader.SKIP_DEBUG);
byte[] data = cw.toByteArray();
File file = new File("G:\WorkSpace\myProject\jvm_test\asm\out\production\asm\com\guanbo\asm");
FileOutputStream fo = new FileOutputStream(file);
fo.write(data);
fo.close();
System.out.println("Generator run success!");
}
}
我们运行看看输出
Generator run success!
运行成功
我们看下生成的class字节码文件
javap -v -p -s -sysinfo -constants Test01.class
我们可以清晰的发现在常量池中已经注入了我们所需要的方法
IDEA解析后的
因此我们的需求 我们的需求已经被植入并生成了Test01.class文件
创建Tset01对象,调用test()方法
我们直接创建一个测试类即可
package com.guanbo.asm;
public class MyTest {
public static void main(String[] args) {
Test01 myTest01 = new Test01();
myTest01.test();
}
}
我们执行以下看下输出结果
这里我们会发这样一个错误
Exception in thread “main” java.lang.VerifyError: Bad local variable type
错误的本地变量类型
我们接着往下看
Reason:
Type top (current frame, locals[1]) is not assignable to long
错误的本地存储类型 不是long
这里我们继续对比
BUG分析对比
这里是我们原始类的class文件被ASM解析后的代码
被注入之后的class文件解析结果
我们发现原class文件中ASTORE[1]的位置被我们强行改为了LSTORE[1]
这就导致对本地变量表造成了影响
那么我们该怎么解决呢?
解决方案
这时候你肯定会在想,那我把原java文件类中的变量删掉不就好了
这是我们的一种解决方案
我们演示一下
package com.guanbo.asm;
public class Test01 {
public void test() throws InterruptedException {
System.out.println("location:com.guanbo.asm.Test01");
// try {
Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
重新Generator一下
Generator run success!
成功写出
我们启动咱们的测试类
package com.guanbo.asm;
public class MyTest {
public static void main(String[] args) throws InterruptedException {
Test01 myTest01 = new Test01();
myTest01.test();
}
}
控制台输出结果:
location:com.guanbo.asm.Test01
invoke method total time ==104
这里我们切入的功能已经成功写入
这里你会有疑问了?
我这不是拆东墙补西墙吗?UP主你这也太拉了吧,真TM无情啊
别慌,咱们还有解决方案
通过引入类,解决本地变量表冲突
创建MyTimeLogger类
package com.guanbo.asm;
public class MyTimeLogger {
public static long a1 = 0L;
public static void start() {
a1 = System.currentTimeMillis();
}
public static void end() {
long a2 = System.currentTimeMillis();
System.out.println("new invoke method total time == " + (a2 - a1));
}
}
我们对原始的Test01.java做下修改,引入MyTimeLogger中的方法
观察ASM自动解析生成的代码
那我们接下来重新定义一个ClassVisitor
package com.guanbo.asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor2 extends ClassVisitor {
public MyClassVisitor2(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
//这里需要过滤掉<init>JVM执行时初始化的方法
if (!"<init>".equals(name) && mv != null) {
//这里便开始植入所需要的功能需求代码
mv = new MyMethodVistor2(mv);
}
return mv;
}
}
class MyMethodVistor2 extends MethodVisitor {
public MyMethodVistor2(MethodVisitor methodVisitor) {
super(Opcodes.ASM7, methodVisitor);
}
@Override
public void visitCode() {
//导入需要植入的指令
mv.visitCode();
//
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/guanbo/asm/MyTimeLogger", "start", "()V", false);
}
@Override
public void visitInsn(int opcode) {
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/guanbo/asm/MyTimeLogger", "end", "()V", false);
}
mv.visitInsn(opcode);
}
}
是不是只需要对类文件添加引入,而无需关心本地变量库的存储了?
那我们重新进行Generator
这里我们把try catch代码块放行
在存在InterruptedException e变量的情况下进行测试
public void test() throws InterruptedException {
System.out.println("location:com.guanbo.asm.Test01");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
因此我们可以将增强的方法,全部封装在一个新的类中
这样就完美的解决了变量冲突问题