ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。
在 ASM 中,提供了一个 Cla***eader 类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept 方法,这个方法接受一个实现了 ClassVisitor 接口的对象实例作为参数,然后依次调用 ClassVisitor 接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,Cla***eader 知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor 会产生一些子过程,比如 visitMethod 会返回一个实现 MethordVisitor 接口的实例,visitField 会返回一个实现 FieldVisitor 接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 Cla***eader 来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 Cla***eader 类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。
各个 ClassVisitor 通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。
ClassAdaptor 类实现了 ClassVisitor 接口所定义的所有函数,当新建一个 ClassAdaptor 对象的时候,需要传入一个实现了 ClassVisitor 接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor 类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。
每个 ClassAdaptor 类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:
  1. 删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod 方法返回的 MethodVisitor 对象。
    class DelLoginClassAdapter extends ClassAdapter {
    	public DelLoginClassAdapter(ClassVisitor cv) {
    		super(cv);
    	}
    	public MethodVisitor visitMethod(final int access, final String name,
    		final String desc, final String signature, final String[] exceptions) {
    		if (name.equals("login")) {
    			return null;
    		}
    		return cv.visitMethod(access, name, desc, signature, exceptions);
    	}
    }
             

  2. 修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。
    class AccessClassAdapter extends ClassAdapter {
    	public AccessClassAdapter(ClassVisitor cv) {
    		super(cv);
    	}
    	public FieldVisitor visitField(final int access, final String name,
            final String desc, final String signature, final Object value) {
            int privateAccess = Opcodes.ACC_PRIVATE;
            return cv.visitField(privateAccess, name, desc, signature, value);
        }
    }
             

  3. 增加新的类、方法、字段
ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了 ClassVisitor 接口,而且含有一个 toByteArray() 函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:
ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter);
ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor);
	
Cla***eader cla***eader = new Cla***eader(strFileName);
cla***eader.accept(classAdapter, Cla***eader.SKIP_DEBUG);
         

综上所述,ASM 的时序图如下:

图 4. ASM – 时序图
AOP 的利器:ASM 3.0 介绍(三)

AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
回页首


我们还是用上面的例子,给 Account 类加上 security check 的功能。与 proxy 编程不同,ASM 不需要将 Account 声明成接口,Account 可以仍旧是一个实现类。ASM 将直接在 Account 类上动手术,给 Account 类的 operation 方法首部加上对 SecurityChecker.checkSecurity 的调用。
首先,我们将从 ClassAdapter 继承一个类。ClassAdapter 是 ASM 框架提供的一个默认类,负责沟通 Cla***eaderClassWriter。如果想要改变 Cla***eader 处读入的类,然后从 ClassWriter 处输出,可以重写相应的 ClassAdapter 函数。这里,为了改变 Account 类的 operation 方法,我们将重写 visitMethdod 方法。
class AddSecurityCheckClassAdapter extends ClassAdapter{
	public AddSecurityCheckClassAdapter(ClassVisitor cv) {
		//Responsechain 的下一个 ClassVisitor,这里我们将传入 ClassWriter,
		//负责改写后代码的输出
		super(cv);
	}
	
	//重写 visitMethod,访问到 "operation" 方法时,
	//给出自定义 MethodVisitor,实际改写方法内容
	public MethodVisitor visitMethod(final int access, final String name,
		final String desc, final String signature, final String[] exceptions) {
		MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error:  The previous line is longer than the max of 90 characters ---------|
		MethodVisitor wrappedMv = mv;
		if (mv != null) {
			//对于 "operation" 方法
			if (name.equals("operation")) { 
				//使用自定义 MethodVisitor,实际改写方法内容
				wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
			} 
		}
		return wrappedMv;
	}
}   

下一步就是定义一个继承自 MethodAdapterAddSecurityCheckMethodAdapter,在“operation”方法首部插入对 SecurityChecker.checkSecurity() 的调用。
class AddSecurityCheckMethodAdapter extends MethodAdapter {
	public AddSecurityCheckMethodAdapter(MethodVisitor mv) {
		super(mv);
	}
	public void visitCode() {
		visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker",
			"checkSecurity", "()V");
	}
}   
 

其中,Cla***eader 读到每个方法的首部时调用 visitCode(),在这个重写方法里,我们用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V"); 插入了安全检查功能。
最后,我们将集成上面定义的 ClassAdapterCla***eaderClassWriter 产生修改后的 Account 类文件:
import java.io.File;
import java.io.FileOutputStream;
import org.objectweb.asm.*;
    
public class Generator{
	public static void main() throws Exception {
		Cla***eader cr = new Cla***eader("Account");
		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
		ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
		cr.accept(classAdapter, Cla***eader.SKIP_DEBUG);
		byte[] data = cw.toByteArray();
		File file = new File("Account.class");
		FileOutputStream fout = new FileOutputStream(file);
		fout.write(data);
		fout.close();
	}
}
 

执行完这段程序后,我们会得到一个新的 Account.class 文件,如果我们使用下面代码:
public class Main {
	public static void main(String[] args) {
		Account account = new Account();
		account.operation();
	}
}
 

使用这个 Account,我们会得到下面的输出:
SecurityChecker.checkSecurity ...
operation...
 

也就是说,在 Account 原来的 operation 内容执行之前,进行了 SecurityChecker.checkSecurity() 检查。
上面给出的例子是直接改造 Account 类本身的,从此 Account 类的 operation 方法必须进行 checkSecurity 检查。但事实上,我们有时仍希望保留原来的 Account 类,因此把生成类定义为原始类的子类是更符合 AOP 原则的做法。下面介绍如何将改造后的类定义为 Account 的子类 Account$EnhancedByASM。其中主要有两项工作:
  • 改变 Class Description, 将其命名为 Account$EnhancedByASM,将其父类指定为 Account
  • 改变构造函数,将其中对父类构造函数的调用转换为对 Account 构造函数的调用。
AddSecurityCheckClassAdapter 类中,将重写 visit 方法:
public void visit(final int version, final int access, final String name,
		final String signature, final String superName,
		final String[] interfaces) {
	String enhancedName = name + "$EnhancedByASM";  //改变类命名
	enhancedSuperName = name; //改变父类,这里是”Account”
	super.visit(version, access, enhancedName, signature,
	enhancedSuperName, interfaces);
}
 

改进 visitMethod 方法,增加对构造函数的处理:
public MethodVisitor visitMethod(final int access, final String name,
	final String desc, final String signature, final String[] exceptions) {
	MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
	MethodVisitor wrappedMv = mv;
	if (mv != null) {
		if (name.equals("operation")) {
			wrappedMv = new AddSecurityCheckMethodAdapter(mv);
		} else if (name.equals("<init>")) {
			wrappedMv = new ChangeToChildConstructorMethodAdapter(mv,
				enhancedSuperName);
		}
	}
	return wrappedMv;
}
 

这里 ChangeToChildConstructorMethodAdapter 将负责把 Account 的构造函数改造成其子类 Account$EnhancedByASM 的构造函数:
class ChangeToChildConstructorMethodAdapter extends MethodAdapter {
	private String superClassName;
	public ChangeToChildConstructorMethodAdapter(MethodVisitor mv,
		String superClassName) {
		super(mv);
		this.superClassName = superClassName;
	}
	public void visitMethodInsn(int opcode, String owner, String name,
		String desc) {
		//调用父类的构造函数时
		if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
			owner = superClassName;
		}
		super.visitMethodInsn(opcode, owner, name, desc);//改写父类为superClassName
	}
}

最后演示一下如何在运行时产生并装入产生的 Account$EnhancedByASM。 我们定义一个 Util 类,作为一个类工厂负责产生有安全检查的 Account 类:
public class SecureAccountGenerator {
	private static AccountGeneratorClassLoader classLoader = 
		new AccountGeneratorClassLoade();
	private static Class secureAccountClass;
	public Account generateSecureAccount() throws ClassFormatError, 
		InstantiationException, IllegalAccessException {
		if (null == secureAccountClass) {            
			Cla***eader cr = new Cla***eader("Account");
			ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
			ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
			cr.accept(classAdapter, Cla***eader.SKIP_DEBUG);
			byte[] data = cw.toByteArray();
			secureAccountClass = classLoader.defineClassFromClassFile(
				"Account$EnhancedByASM",data);
		}
		return (Account) secureAccountClass.newInstance();
	}
	private static class AccountGeneratorClassLoader extends ClassLoader {
		public Class defineClassFromClassFile(String className,
			byte[] classFile) throws ClassFormatError {
			return defineClass("Account$EnhancedByASM", classFile, 0, classFile.length());
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error:  The previous line is longer than the max of 90 characters ---------|
		}
	}
}

静态方法 SecureAccountGenerator.generateSecureAccount() 在运行时动态生成一个加上了安全检查的 Account 子类。著名的 Hibernate 和 Spring 框架,就是使用这种技术实现了 AOP 的“无损注入”。

AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
AOP 的利器:ASM 3.0 介绍(三)
回页首


最后,我们比较一下 ASM 和其他实现 AOP 的底层技术:

表 1. AOP 底层技术比较
AOP 底层技术 功能 性能 面向接口编程 编程难度
直接改写 class 文件 完全控制类 无明显性能代价 不要求 高,要求对 class 文件结构和 Java 字节码有深刻了解
JDK Instrument 完全控制类 无论是否改写,每个类装入时都要执行hook程序 不要求 高,要求对 class 文件结构和 Java 字节码有深刻了解
JDK Proxy 只能改写 method 反射引入性能代价 要求
ASM 几乎能完全控制类 无明显性能代价 不要求 中,能操纵需要改写部分的 Java 字节码