前言
- 官方介绍:ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or dynamically generate classes, directly in binary form
- ASM是一个多用途的Java字节码操作和分析框架。它可以被用来修改现有类或动态生成的类,直接以二进制形式
- ASM 应该是 Assembly缩写(官方没有缩写介绍,个人理解)
哪些著名的框架使用了ASM
- Languages and AOP tools: AspectWerkz | AspectJ | BeanShell | CGLIB | dynaop | Clojure | Groovy
- Java ME: EclipseME | MicroEmulator Sun Java ME emulation for Java SE |
- Tools and frameworks: Fractal | Dr. Garbage | Proactive | Retrotranslator | RIFE | R-OSGi | Terracotta
- Persistence: EasyBeans | Ebean | JDBCPersistence | JPOX | OpenEJB | Oracle BerkleyDB
- Monitoring: BEA WebLogic | BTrace | Byteman | JiP | ByCounter | Limpid Log
- Testing and code analysis: Agitar | Cobertura | Eclipse | JCarder | SemmleCode | Structure101 | SonarJ
ASM特点
ASM特点
- 短小精悍、快速、健壮
- 具有很好的文档,并且还有eclipse\Idea插件
- 开源
ASM Core Api 设计
主要的类介绍(ASM4.0)
- ClassVisitor:重要的抽象类,定义在读取Class字节码时会触发的事件,如类头部解析完成、注解解析、字段解析、方法解析等
- ClassReader:重要的类,字节码的读取与分析引擎。它采用类似SAX的事件读取机制,每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理
- FieldVisitor:抽象类,定义在解析类属性时触发的事件
- MethodVisitor接口:定义在解析方法时触发的事件,如方法上的注解、属性、代码等
- ClassWriter类:实现了ClassVisitor接口,用于拼接字节码
- FieldWriter类:实现了FieldVisitor接口,用于拼接类属性相关字节码
- MethodWriter类:实现了MethodVisitor接口,用于拼接方法相关字节码
- Opcodes接口:字节码指令的一些常量定义
- Type类:类型相关的常量定义以及一些基于其上的操作
ClassVisitor类介绍
ClassVisitor 是重要的抽象类,在ASM4.0版本中是抽象方法,调用必须是遵循下面的调用顺序的:1
2visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )
( visitInnerClass | visitField | visitMethod ) visitEnd
- 方法void visit(int version,int access,String name,String signature,String superName,String[] interfaces);
当调用reader.accept()扫描类时第一个调用的方法就是visit,主要用于类声明时使用。参数的含义如下:
- version JDK的版本,V1_6表示JDK1.6版本
- access表示类的修饰符,public, private,static 等等
- name表示类名称
- signature表示泛型信息
- superName表示继承父类的名称
- interfaces表示实现接口的名称数组
- FieldVisitor visitField(int access,String name,String desc,String signature,Object value)
visitField是调用accept()方法扫描到类中字段时进行调用的。主要参数的含义如下:
- name表示字段名称
- desc表示字段的类型,I(int)、Z(boolean)等等
- value表示是默认值
- MethodVisitor visitMethod(int access,String name,String desc,String signature,String[] exceptions)
visitMethod是调用accept()方法扫描到类中字段时进行调用的。主要参数含义如下:
- name表示方法名称
- desc表示方法返回值类型
- exceptions表示方法会抛出的异常数组
- 类型的简写对应关系
1
2
3
4
5
6
7
8
9
10
11
12
13"I" = int
"B" = byte
"C" = char
"D" = double
"F" = float
"J" = long
"S" = short
"Z" = boolean
"V" = void
"[...;" = 数组
"[[...;" = 二维数组
"[[[...;" = 三维数组
"L....;" = 引用类型
ClassReader实现
ClassReader是ASM中最核心的实现类,它作用是去解析Class字节码。当ClassReader解析到Method、Field、Annotation等都会触发事件。1
2ClassReader reader = new ClassReader("com.alibaba.Apple");
reader.accept(enhancer, ClassReader.SKIP_DEBUG);
ClassReader的构造函数的参数是class的文件流或者classPath。通过accept()方法解析上述的文件流,每当有事件发生时,会调用注册的继承于ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor等实现类的相应方法。accept()方法中关键代码1
2
3
4
5
6
7
8
9
10// calls the visit method
classVisitor.visit(readInt(4),access,name,signature,superClassName,implementedItfs);
// calls the visitSource method
if (!skipDebug && (sourceFile != null || sourceDebug != null)) {
classVisitor.visitSource(sourceFile, sourceDebug);
}
// calls the visitOuterClass method
if (enclosingOwner != null) {
classVisitor.visitOuterClass(enclosingOwner,enclosingName,enclosingDesc);
}
上述的代码我们也可以看出ClassVisitor调用顺序visitSource、visitOuterClass、visitAnnotation ………..visitEnd。
关于Java字节码格式可以参考这篇博客
ClassWriter类
ClassWriter继承于ClassVisitor,提供很多修改字节码的方法;通过toByteArray()方法可以得到字节码的byte数组,然后用类加载器动态加载代理类class,如下:1
2byte[] code = cw.toByteArray();
Class newClass = defineClass(proxyClassName, code, 0, code.length);
工具类ClassPrinter(打印class扫描阶段的基本信息)
1 | public class ClassPrinter extends ClassVisitor { |
运行结果
1 | com/alibaba/Apple extends java/lang/Object { |
例子:类生成
我们生成这样Apple类,有个字段String name,有个方法testApple(String param),具体如下:1
2
3
4
5
6public class Apple {
private String name;
public void testApple(String param){
System.out.println("testApple: "+param);
}
}
生成这样一个类,需要调用ClassVisitor和ClassWriter等工具类,然后使用ClassLoader加载类,具体实现的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63public class ClassGenTest extends ClassLoader implements Opcodes {
public byte[] genBytes(){
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, "com/alibaba/Apple", null, "java/lang/Object", null);
cw.visitSource("Apple.java", null);
// 定义字段name
{
fv = cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
fv.visitEnd();
}
// 默认构造函数
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(11, l1);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
// 定义函数public void testApp(String)
{
mv = cw.visitMethod(ACC_PUBLIC, "testApple", "(Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V");
mv.visitLdcInsn("testApple: ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitMaxs(3, 2);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
public Class genClass(){
byte[] bytes = this.genBytes();
return this.defineClass(bytes, 0, bytes.length);
}
public static void main(String[] agrs) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassGenTest classGenTest = new ClassGenTest();
Class proxyClass = classGenTest.genClass();
// 测试生成的Apple类的实例,能否调用testApple
Object proxyObject = proxyClass.newInstance();
Method method = proxyClass.getDeclaredMethod("testApple", new Class[]{String.class});
method.invoke(proxyObject, "hello asm");
}
}
使用 ASM 实现 Java 语言的“多重继承”
http://www.ibm.com/developerworks/cn/java/j-lo-asm/
插件ByteCode Outline使用介绍
我们看到生成Apple类是用调用了很多ClassVisitor和ClassWriter的方法,看上去比较的复杂,也标的繁琐。实际开发中我们先使用工具大致生成构建代码,然后修改。
博主使用的工具是ByteCode Outline,它是一款IDE插件,在Eclipse和IDEA下都有的插件。在IDEA下安装和使用方法方法如下:打开插件管理File | Settings | Plugins
JDK Instrument功能介绍简介
我们使用ASM,Cglib或者JDK Dynamic Proxy的时候经常会生产代理类,如何才能查看这些代理类的代码呢?Java5开始引入了“java.lang.instrument”包,在更底层实现字节码拦截功能,我们也可以用instrument包来修改字节码。
Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。
如何使用Instrument功能
1 | public class DumpClassFileTransformer implements ClassFileTransformer { |
把DumpClassFileTransformer类打成jar包(可以使用Eclipse或者IDEA的带出为jar的功能),目录结构如下
1 | aop.jar |
MANIFEST.MF文件内容只有一行:Manifest-Version: 1.0
运行时需要添加JVM参数
1 | -javaagent:D:\code\proxy\aop.jar |