ASM 是什么
ASM 官方 的定义是:
ASM是一个通用的 Java 字节码操控和分析框架。它可以用于修改已有的类也可以直接生成类。ASM 提供了一些常用的字节码转换和分析算法,从中可以构建自定义的复杂转换和源码分析工具。ASM提供了与其他 Java 字节码框架类似的方法,但是更注重性能。因为它被设计和实现成尽可能小和快,所以非常适用于动态系统(当然也可以用于静态的方式,例如在编译器中)。
总之ASM让我们可以对 Java 字节码做几乎任何事情。例如获取类文件的详细信息、动态修改或生成一个类等。
Java 是一门静态语言,所有的类在编译时就已经给定了,但是有很多情况下,我们需要在运行时动态地生成或者增强一些类。这时候 ASM 就派上用场了。
为什么要用 ASM
最典型的应用场景是 AOP(面向切面编程)。实际开发中我们会发现,项目中经常存在这么一类零散而又耦合的代码。例如打印方法耗时、方法权限控制等。
假设这样的场景,团队规定对所有类的关键的方法都必须打印方法执行耗时,如果方法执行抛出异常则必须把参数也打印出来。
而对这类需求,用传统的做法我们会发现整个项目充斥着下面这段重复的模板代码:
try {
long start = System.currentTimeMillis();
// 业务代码
...
long timeConsumed = start - System.currentTimeMillis();
log.info("方法 {} 耗时: {}", "doSomething", timeConsumed)
} catch(Exception e){
log.warn("方法 {} 执行抛出异常:{}, 参数: {}", "doSomething", e.getMessage());
throw e;
}
根据 DRY(Don’t Repeat Yourself) 原则,当我们发现项目中不断出现重复代码时,肯定是哪里出了问题,该优化了。就这个案例来说,如果我们可以在编译或者运行时在业务代码的前面和后面自动加上我们定义好的几行日志代码,那么这些重复的代码就能统统消灭掉了。
AOP 就是为了解决这类问题而生的,它能让我们在方法的执行前后添加我们需要添加的代码,甚至改变方法的执行流程。AOP 的实现跟字节码操控密切相关,SpringAOP 是基于动态代理和CGLIB实现的,而CGLIB是基于ASM实现的。所以我们非常有必要学习字节码操控技术。这个领域使用最广泛的框架就是 ASM。
从 HelloWorld 开始
HelloWorld 是所有人在学习一门语言时写的第一个程序,而学习任何一项新技术时,HelloWorld 也是最好的敲门砖。所以让我们用 ASM 来写一个 HelloWorld 程序吧!
还记得这个 Java 的 HelloWorld 吗?
public class HelloWorld{
public static void main(){
System.out.println("HelloWorld!");
}
}
我们不直接写上面这个类,而是使用 ASM 框架来动态生成:
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* @author dadiyang
* @date 2019/1/13
*/
public class HelloWorldByAsm {
public static void main(String[] args) throws Exception {
// 生成二进制字节码
byte[] bytes = generate();
// 使用自定义的ClassLoader
MyClassLoader cl = new MyClassLoader();
// 加载我们生成的 HelloWorld 类
Class<?> clazz = cl.defineClass("com.dadiyang.asm.HelloWorld", bytes);
// 反射获取 main 方法
Method main = clazz.getMethod("main", String[].class);
// 调用 main 方法
main.invoke(null, new Object[]{new String[]{}});
}
private static byte[] generate() {
ClassWriter cw = new ClassWriter(0);
// 定义对象头:版本号、修饰符、全类名、签名、父类、实现的接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/dadiyang/asm/HelloWorld",
null, "java/lang/Object", null);
// 添加方法:修饰符、方法名、描述符、签名、抛出的异常
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main",
"([Ljava/lang/String;)V", null, null);
// 执行指令:获取静态属性
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载常量 load constant
mv.visitLdcInsn("HelloWorld!");
// 调用方法
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 返回
mv.visitInsn(Opcodes.RETURN);
// 设置栈大小和局部变量表大小
mv.visitMaxs(2, 1);
// 方法结束
mv.visitEnd();
// 类完成
cw.visitEnd();
// 生成字节数组
return cw.toByteArray();
}
}
/**
* 自定义ClassLoader以支持加载字节数组形式的字节码
* @author dadiyang
*/
class MyClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] b) {
// ClassLoader是个抽象类,而ClassLoader.defineClass 方法是protected的
// 所以我们需要定义一个子类将这个方法暴露出来
return super.defineClass(name, b, 0, b.length);
}
}
这时执行程序就会输出 HelloWorld 了。是不是很神奇?我们并没有写一个真正的 HelloWorld.java 文件,也没有在运行之前对其进行编译,但是 ASM 让我们有能力在运行的过程中动态生成一个新的类并加载和运行它。
关于 ASM 入门和API的使用,建议阅读官方的 用户手册。这个手册写得非常不错。