从Java字节码到ASM实践

从Java字节码到ASM实践

参考资料:

Java 字节码 & 虚拟机

1. Java 字节码

就像C或C++编译器将源码编译为汇编码,Java编译器会将Java源码编译成字节码。使用 javac 可以将 .java 文件编译成 .class 文件,.class 文件中存放的就是该 .java 文件对应的字节码内容,比如如下一段 Demo.java 代码:

package com.lijiankun24.classpractice;

public class Demo {

    private int m;

    public int inc() {
        return m + 1;
    }
}

通过 javac 编译生成对应的 Demo.class 文件,使用纯文本文件打开 Demo.class,其中的内容是以 8 位字节为基础单位的二进制流,表面来看就是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的

cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4465 6d6f 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 4465 6d6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0600
0100 0d00 0000 0200 0e

如果再使用 javap -verbose Demo.class 查看该 Demo.class 中的内容,如下图所示

在这里插入图片描述

从上图中,我们可以看到,.class 文件中主要有常量池、字段表、方法表和属性表等内容。如何从以 8 位字节为基础单位的二进制流中分析出常量池、方法表的内容呢?在这篇文章中有详细的介绍 认识 .class 文件的字节码结构

也可以通过javap -c Demo > Demo.bc产生字节码。

一个 Java 类文件大致可以归为 10 个项:

  • Magic: 该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
  • Version: 该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
  • Constant Pool: 该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
  • Access_flag: 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
  • This Class: 指向表示该类全限定名称的字符串常量的指针。
  • Super Class: 指向表示父类全限定名称的字符串常量的指针。
  • Interfaces: 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
  • Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
  • Methods: 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
  • Class attributes: 该项存放了在该文件中类或接口所定义的属性的基本信息。

2. Java 虚拟机类加载机制

上面一小节介绍了 .class 文件的结构,但是 .class 文件是静态的,它最终是会被虚拟机加载才能执行的,那么问题来了,.class 文件是什么时候会被加载呢?

一般来说,一个 .class 文件就包含一个 Java 类,.class 文件和 Java 类是息息相关的。要说 .class 文件的加载时机,就不得不提到 Java 类的生命周期了。想必大家都知道,Java 类的生命周期包含加载验证准备解析初始化使用卸载七个步骤,在 Java 虚拟机规范中并没有规定 Java 类的加载时机,但是却规定了 Java 类 初始化 的时机,而加载又一定是在初始化的前面,所以也可以说是间接地规定了 .class 文件的加载的时机。

有五种情况,是必须初始化一个类的,这五种情况被称为对 Java 类的主动引用,除了 主动引用 之外,其他的对 Java 类的引用称为 被动引用

上面也提到了 Java 类的生命周期总共分为加载验证准备解析初始化使用卸载,其中最重要的是前五个步骤加载验证准备解析初始化,那在这五个步骤中都发生了什么事情呢?

举一个简单的例子,如下所示。下面的 Constant 类中,有一个静态 static 代码块,和一个静态 static 变量, 是什么时候给 value 赋值的呢?什么时候会执行 static 代码块呢?答案是在类的 初始化 阶段。

public class Constant {

    static {
        System.out.println("Constant init!");
    }

    public static String value = "lijiankun24!";
}

在 Java 类中,如果有静态 static 代码块、静态 static 变量的话,编译器会为这个类自动生成一个类构造器(注意,不是实例构造器),在 类构造器 中会执行静态 static 代码块,初始化静态 static 变量,类构造器 就是在类的 初始化 阶段执行的

提到 Java 类的加载,就不得不说起 Java 中的类加载器 ClassLoader 了,双亲委派模型及其好处也是必须要清楚的。

上面只是粗略的介绍,更多想了解五种主动引用、类的生命周期、类构造器、类加载器、双亲委派模型,如果想了解的更详细,请看这篇文章 理解 JVM 中的类加载机制

3. Java 虚拟机字节码执行引擎

Java 内存模型中,非常重要的一个区域就是 Java 虚拟机栈。Java 中每一个方法执行的时候都会在 Java 虚拟机栈中压入一个栈帧,方法执行完成之后,也会将该栈帧出栈。
栈帧中最主要的是局部变量表操作数栈这两个概念,在执行一个 Java 方法的字节码时,其实就是调用 Java 字节码指令操纵局部变量表操作数栈,最后将执行的结果返回。

除了方法的执行过程,还需要了解一下 Java 中的方法调用。方法调用就是指通过 .class 文件中方法的符号引用,确认方法的直接引用的过程,这个过程有可能发生在加载阶段,也有可能发生在运行阶段。

有一些方法是在加载阶段就已经确定了方法的直接引用,比如:静态方法、私有方法、实例构造器方法,这类方法的调用称为 解析;除了解析,方法的 静态分派 也是在加载阶段就确定了方法的直接引用,这类方法常见的就是 重载 的方法。

有一些方法是在运行阶段确认方法的直接引用的,比如:重写 的方法,调用重写 的方法时,需要具体到对象的实际类型,所以需要特定的 Java 字节码 invokevirtual 去确定合适的方法。

Java 虚拟机是基于栈的解释执行的,这里所说的 就是 Java 虚拟机栈,解释执行时相对于编译执行而言的,解释执行就是指:代码通过编译生成字节码指令集之后,通过解释器解释执行的。这个不用了解的太深,明白这几个定义就好

上面介绍了 Java 虚拟机栈中的 栈帧方法调用解析静态分派动态分派 和 Java 虚拟机基于栈的解释执行,详细的内容可以参考 虚拟机字节码执行引擎

AOP编程

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

为什么要动态生成 Java 类?

动态生成 Java 类与 AOP 密切相关的。AOP 的初衷在于软件设计世界中存在这么一类代码,零散而又耦合:零散是由于一些公有的功能(诸如著名的 log 例子)分散在所有模块之中;同时改变 log 功能又会影响到所有的模块。出现这样的缺陷,很大程度上是由于传统的 面向对象编程注重以继承关系为代表的“纵向”关系,而对于拥有相同功能或者说方面 (Aspect)的模块之间的“横向”关系不能很好地表达。例如,目前有一个既有的银行管理系统,包括 Bank、Customer、Account、Invoice 等对象,现在要加入一个安全检查模块,对已有类的所有操作之前都必须进行一次安全检查。

图 1. ASM – AOP

然而 Bank、Customer、Account、Invoice 是代表不同的事务,派生自不同的父类,很难在高层上加入关于 Security Checker 的共有功能。对于没有多继承的 Java 来说,更是如此。传统的解决方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍旧是分散的 —— 每个需要 Security Checker 的类都必须要派生一个 Decorator,每个需要 Security Checker 的方法都要被包装(wrap)。下面我们以 Account 类为例看一下Decorator:

首先,我们有一个 SecurityChecker 类,其静态方法 checkSecurity 执行安全检查功能:

public class SecurityChecker {
	public static void checkSecurity() {
		System.out.println("SecurityChecker.checkSecurity ...");
		//TODO real security check
	}	
}

另一个是 Account 类:

public class Account {
	public void operation() {
		System.out.println("operation...");
		//TODO real operation
	}
}

若想对 operation 加入对 SecurityCheck.checkSecurity() 调用,标准的 Decorator 需要先定义一个 Account类的接口:

public interface Account {
	void operation(); 
}

然后把原来的 Account 类定义为一个实现类:

public class AccountImpl extends Account{
	public void operation() {
		System.out.println("operation...");
		//TODO real operation
	}
} 

定义一个 Account 类的 Decorator,并包装 operation 方法:

public class AccountWithSecurityCheck implements Account {	
	private  Account account;
	public AccountWithSecurityCheck (Account account) {
		this.account = account;
	}
	public void operation() {
		SecurityChecker.checkSecurity();
		account.operation();
	}
}

在这个简单的例子里,改造一个类的一个方法还好,如果是变动整个模块,Decorator 很快就会演化成另一个噩梦。动态改变 Java 类就是要解决 AOP 的问题,提供一种得到系统支持的可编程的方法,自动化地生成或者增强 Java 代码。这种技术已经广泛应用于最新的 Java 框架内,如 Hibernate,Spring 等。

使用 Proxy 改造 Java 类

最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了class 文件的格式,直接编辑字节码确实可以改变 Java 类的行为。直到今天,还有一些 Java 高手们使用最原始的工具,如 UltraEdit 这样的编辑器对 class 文件动手术。是的,这是最直接的方法,但是要求使用者对 Java class 文件的格式了熟于心:小心地推算出想改造的函数相对文件首部的偏移量,同时重新计算 class 文件的校验码以通过 Java 虚拟机的安全机制。

Java 5 中提供的 Instrument 包也可以提供类似的功能:启动时往 Java 虚拟机中挂上一个用户定义的 hook 程序,可以在装入特定类的时候改变特定类的字节码,从而改变该类的行为。但是其缺点也是明显的:

  • Instrument 包是在整个虚拟机上挂了一个钩子程序,每次装入一个新类的时候,都必须执行一遍这段程序,即使这个类不需要改变。
  • 直接改变字节码事实上类似于直接改写 class 文件,无论是调用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),还是 Instrument.redefineClasses(ClassDefinition[] definitions),都必须提供新 Java 类的字节码。也就是说,同直接改写 class 文件一样,使用 Instrument 也必须了解想改造的方法相对类首部的偏移量,才能在适当的位置上插入新的代码。

尽管 Instrument 可以改造类,但事实上,Instrument 更适用于监控和控制虚拟机的行为。

一种比较理想且流行的方法是使用 java.lang.ref.proxy。我们仍旧使用上面的例子,给 Account 类加上 checkSecurity 功能:

首先,Proxy 编程是面向接口的。下面我们会看到,Proxy 并不负责实例化对象,和 Decorator 模式一样,要把Account 定义成一个接口,然后在 AccountImpl 里实现 Account 接口,接着实现一个 InvocationHandler``Account 方法被调用的时候,虚拟机都会实际调用这个 InvocationHandlerinvoke 方法:

class SecurityProxyInvocationHandler implements InvocationHandler {
	private Object proxyedObject;
	public SecurityProxyInvocationHandler(Object o) {
		proxyedObject = o;
	}

	public Object invoke(Object object, Method method, Object[] arguments)
		throws Throwable {			
		if (object instanceof Account && method.getName().equals("opertaion")) {
			SecurityChecker.checkSecurity();
		}
		return method.invoke(proxyedObject, arguments);
	}
}	

最后,在应用程序中指定 InvocationHandler 生成代理对象:

public static void main(String[] args) {
	Account account = (Account) Proxy.newProxyInstance(
		Account.class.getClassLoader(),
		new Class[] { Account.class },
		new SecurityProxyInvocationHandler(new AccountImpl())
	);
	account.function();
}

其不足之处在于:

  • Proxy 是面向接口的,所有使用 Proxy 的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的:Proxy 生成的对象是接口一致的而不是对象一致的:例子中 Proxy.newProxyInstance 生成的是实现 Account 接口的对象而不是 AccountImpl 的子类。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。
  • Proxy 毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍。而且,从程序实现上可以看出,对 proxy class 的所有方法调用都要通过使用反射的 invoke 方法。因此,对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈。

AOP 底层技术比较

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

ASM & 访问者模式

1. ASM 简介

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。

相比于其他流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具有类似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分别有 350k 和 150k。同时,同样类转换的负载,如果 ASM 是 60% 的话,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

ASM 库的结构如下所示:

在这里插入图片描述

  • Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和 ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类.
  • Tree:提供了 Java 字节码在内存中的表现
  • Commons:提供了一些常用的简化字节码生成、转换的类和适配器
  • Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用
  • XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化

2. 访问者模式

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。

2.1 概述 & 定义
  1. 定义:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些数据元素的新的操作

  2. 意图:主要将数据结构和数据操作分离

  3. 主要解决:稳定的数据结构和易变的操作的解耦

  4. 适用场景:

    • 假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,可以使用访问者模式把这些操作封装到访问者中去,这样便避免了这些不相干的操作污染这个对象。
    • 假如一组对象中,存在着相似的操作,可以将这些相似的操作封装到访问者中去,这样便避免了出现大量重复的代码
    • 访问者模式适用于对功能已经确定的项目进行重构的时候适用,因为功能已经确定,元素类的数据结构也基本不会变了;如果是一个新的正在开发中的项目,在访问者模式中,每一个元素类都有它对应的处理方法,每增加一个元素类都需要修改访问者类,修改起来相当麻烦。
2.2 示例

如果老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖;如果老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。

在这个例子中,老师和学生就是Element,他们的数据结构稳定不变。从上面的描述中,我们发现,对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这样就适合使用访问者模式来分离数据结构和操作。

2.2.1 创建抽象元素
public interface Element {
    void accept(Visitor visitor);
}
2.2.2 创建具体元素

创建两个具体元素 Student 和 Teacher,分别实现 Element 接口

public class Student implements Element {
    private String name;
    private int grade;
    private int paperCount;

    public Student(String name, int grade, int paperCount) {
        this.name = name;
        this.grade = grade;
        this.paperCount = paperCount;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    ......

}
public class Teacher implements Element {
    private String name;
    private int score;
    private int paperCount;

    public Teacher(String name, int score, int paperCount) {
        this.name = name;
        this.score = score;
        this.paperCount = paperCount;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

        ......

}
2.2.3 创建抽象访问者
public interface Visitor {

    void visit(Student student);

    void visit(Teacher teacher);
}
2.2.4 创建具体访问者

创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口

public class GradeSelection implements Visitor {

    @Override
    public void visit(Student student) {
        if (student != null && student.getGrade() >= 90) {
            System.out.println(student.getName() + "的分数是" + student.getGrade() + ",荣获了成绩优秀奖。");
        }
    }

    @Override
    public void visit(Teacher teacher) {
        if (teacher != null && teacher.getScore() >= 85) {
            System.out.println(teacher.getName() + "的分数是" + teacher.getScore() + ",荣获了成绩优秀奖。");
        }
    }
}
2.2.5 访问者代码调用
public class VisitorClient {

    public static void main(String[] args) {
        Element element = new Student("lijiankun24", 90, 3);

        Visitor visitor = new GradeSelection();
        element.accept(visitor);
    }
}

上述代码即是一个简单的访问者模式的示例代码,输出如下所示:

lijiankun24的分数是90,荣获了成绩优秀奖。

上述代码可以分为三步:

  1. 创建一个元素类的对象
  2. 创建一个访问类的对象
  3. 元素对象通过 Element#accept(Visitor visitor) 方法传入访问者对象
2.3 ASM 中的访问者模式
2.3.1 ASM 中几个重要的类

在 ASM 库中存在以下几个重要的类:

  • ClassReader:它将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域。可以将 ClassReader 看作是 Visitor 模式中的访问者的实现类

  • ClassVisitor(抽象类):ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit() 方法,从而实现对字节码的修改。在 ClassVisitor 中的一些访问会产生子过程,比如 visitMethod 会产生 MethodVisitor 的调用,visitField 会产生对 FieldVisitor 的调用,用户也可以对这些 Visitor 进行自己的实现,从而达到对这些子节点的字节码的访问和修改。

    在 ASM 的访问者模式中,用户还可以提供多种不同操作的 ClassVisitor 的实现,并以责任链的模式提供给 ClassReader 来使用,而 ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor。

  • ClassWriter:ClassWriter 是 ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改,而 ClassWriter 的操作则是老实得把每一个节点修改后的字节码输出为字节数组。

2. 3.2 ASM 的工作流程

ASM 大致的工作流程是:

  1. ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素
  2. 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor
  3. 然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改
  4. 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作

3. Core API 介绍

ASM 提供了两种编程模型:

  • Core API,提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存。但这种编程方式难度较大。
  • Tree API,提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方法需要更多的内存。这种编程方式较简单。
3.1 ClassVisitor 抽象类

如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd

public abstract class ClassVisitor {

        ......

    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);
    public AnnotationVisitor visitAnnotation(String desc, boolean visible);
    public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName, String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
    public void visitEnd();
}
  1. void visit(int version, int access, String name, String signature, String superName, String[] interfaces)

    该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口)

  2. AnnotationVisitor visitAnnotation(String desc, boolean visible)

    该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。

  3. FieldVisitor visitField(int access, String name, String desc, String signature, Object value)

    该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)

  4. MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)

    该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常)

  5. void visitEnd()

    该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

3.2 ClassReader 类

这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法

3.3 ClassWriter 类

ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

3.4 MethodVisitor & AdviceAdapter

MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。

AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。

AdviceAdapter 比较重要的几个方法如下:

  1. void visitCode():表示 ASM 开始扫描这个方法
  2. void onMethodEnter():进入这个方法
  3. void onMethodExit():即将从这个方法出去
  4. void onVisitEnd():表示方法扫码完毕
3.5 FieldVisitor 抽象类

FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍.

3.6 操作流程
  1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
  2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
  3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

4. 示例

4.1 修改类中方法的字节码

假如现在我们有一个 HelloWorld 类,如下

package com.lijiankun24.asmpractice.demo;

public class HelloWorld {

    public void sayHello() {
        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通过 javac HelloWorld.javajavap -verbose HelloWorld.class 可以查看到 sayName() 方法的字节码如下所示:

public void sayHello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc2_w        #2                  // long 2000l
         3: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
         6: goto          14
         9: astore_1
        10: aload_1
        11: invokevirtual #6                  // Method java/lang/InterruptedException.printStackTrace:()V
        14: return
      Exception table:
         from    to  target type
             0     6     9   Class java/lang/InterruptedException
      LineNumberTable:
        line 5: 0
        line 8: 6
        line 6: 9
        line 7: 10
        line 9: 14
      StackMapTable: number_of_entries = 2
        frame_type = 73 /* same_locals_1_stack_item */
          stack = [ class java/lang/InterruptedException ]
        frame_type = 4 /* same */

我们通过 ASM 修改 HelloWorld.class 字节码文件,实现统计方法执行时间的功能

public class CostTime {

    public static void main(String[] args) {
        redefinePersonClass();
    }

    private static void redefinePersonClass() {
        String className = "com.lijiankun24.asmpractice.demo.HelloWorld";
        try {
            InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/HelloWorld.class");
            ClassReader reader = new ClassReader(inputStream);                               // 1. 创建 ClassReader 读入 .class 文件到内存中
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);                 // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写
            ClassVisitor change = new ChangeVisitor(writer);                                        // 3. 创建自定义的 ClassVisitor 对象
            reader.accept(change, ClassReader.EXPAND_FRAMES);                                       // 4. 将 ClassVisitor 对象传入 ClassReader 中

            Class clazz = new MyClassLoader().defineClass(className, writer.toByteArray());
            Object personObj = clazz.newInstance();
            Method nameMethod = clazz.getDeclaredMethod("sayHello", null);
            nameMethod.invoke(personObj, null);
            System.out.println("Success!");
            byte[] code = writer.toByteArray();                                                               // 获取修改后的 class 文件对应的字节数组
            try {
                FileOutputStream fos = new FileOutputStream("/Users/lijiankun/Desktop/HelloWorld2.class");    // 将二进制流写到本地磁盘上
                fos.write(code);
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("Failure!");
        }
    }

    static class ChangeVisitor extends ClassVisitor {

        ChangeVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
            if (name.equals("<init>")) {
                return methodVisitor;
            }
            return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
        }
    }

    static class ChangeAdapter extends AdviceAdapter {
        private int startTimeId = -1;

        private String methodName = null;

        ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
            methodName = name;
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            startTimeId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitIntInsn(LSTORE, startTimeId);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            int durationId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, startTimeId);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationId);
            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", false);
            mv.visitLdcInsn("The cost time of " + methodName + " is ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationId);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }
}

执行结果如下图所示

在这里插入图片描述

反编译 HelloWorld2.class 文件的内容如下所示

在这里插入图片描述

4.2 修改类中属性的字节码

这一节中我们将展示一下如何使用 Core API 对类中的属性进行操作。

假如说,现在有一个 Person.java 类如下所示:

public class Person {
    public String name;
    public int sex;
}

我们想为这个类,添加一个 ‘public int age’ 的属性该怎么添加呢?我们会面对两个问题:

  1. 该调用 ASM 的哪个 API 添加属性呢?
  2. 在何时写添加属性的代码?

接下来,我们就一一解决上面的两个问题?

4.2.1 添加属性的 API

按照我们分析的上述的 3.6 操作流程叙述,需要以下三个步骤:

  1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
  2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
  3. 需要创建一个事件过滤器 ClassVisitor。事件过滤器中的某些方法可以产生一个新的XXXVisitor对象,当我们需要修改对应的内容时只要实现自己的XXXVisitor并返回就可以了

在上面三个步骤中,可以操作的就是 ClassVisitor 了。ClassVisitor 接口提供了和类结构同名的一些方法,这些方法可以对相应的类结构进行操作。

在使用 ClassVisitor 添加类属性的时候,只需要添加一句话就可以了:

classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
4.2.2 添加属性的时机

我们先暂且在 ClassVisitor 的 visitEnd() 方法中写入上面的代码,如下所示

public class Transform extends ClassVisitor {  

    public Transform(ClassVisitor cv) {  
        super(cv);  
    }  

    @Override  
    public void visitEnd() {  
        cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);  
    }  
}

我们写如下的测试类,测试一下

public class FieldPractice {

    public static void main(String[] args) {
        addAgeField();
    }

    private static void addAgeField() {
        try {
            InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/Person.class");
            ClassReader reader = new ClassReader(inputStream);

            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

            ClassVisitor visitor = new Transform(writer);
            reader.accept(visitor, ClassReader.SKIP_DEBUG);

            byte[] classFile = writer.toByteArray();
            MyClassLoader classLoader = new MyClassLoader();
            Class clazz = classLoader.defineClass("Person", classFile);
            Object obj = clazz.newInstance();

            System.out.println(clazz.getDeclaredField("name").get(obj)); //----(1)
            System.out.println(clazz.getDeclaredField("age").get(obj));  //----(2)
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其输出入下所示:

null
0

那如果我们尝试在 ClassVisitor#visitField() 方法中添加属性可以吗?我们可以修改 Transform 测试一下:

public class Transform extends ClassVisitor {

    Transform(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
        return super.visitField(access, name, desc, signature, value);
    }
}

还是使用上面的测试代码测试一下,会有如下的测试结果

在这里插入图片描述

在 Person 类中有重复的属性,为什么会报这个错误呢?

分析 ClassVisitor#visitField() 方法可得知,只要访问类中的一个属性,visitField() 方法就会被调用一次,在 Person 类中有两个属性,所以 visitField() 方法就会被调用两次,也就添加了两次 ‘public int age’ 属性,就报了上述的错误,而 visitEnd() 方法只有在最后才会被调用且只调用一次,所以在 visitEnd() 方法中是添加属性的最佳时机。

4.3 生成类
package com.asm3;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

/**
 * 通过asm生成类的字节码
 * @author Administrator
 *
 */
public class GeneratorClass {

    public static void main(String[] args) throws IOException {
        //生成一个类只需要ClassWriter组件即可
        ClassWriter cw = new ClassWriter(0);
        //通过visit方法确定类的头部信息
        cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,
                "com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});
        //定义类的属性
        cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
                "LESS", "I", null, new Integer(-1)).visitEnd();
        cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
                "EQUAL", "I", null, new Integer(0)).visitEnd();
        cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
                "GREATER", "I", null, new Integer(1)).visitEnd();
        //定义类的方法
        cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "compareTo",
                "(Ljava/lang/Object;)I", null, null).visitEnd();
        cw.visitEnd(); //使cw类已经完成
        //将cw转换成字节数组写到文件里面去
        byte[] data = cw.toByteArray();
        File file = new File("D://Comparable.class");
        FileOutputStream fout = new FileOutputStream(file);
        fout.write(data);
        fout.close();
    }
}

生成一个类的字节码文件只需要用到ClassWriter类即可,生成Comparable.class后用javap指令对其进行反编译:javap -c Comparable.class >test.txt ,编译后的结果如下:

public interface com.asm3.Comparable extends com.asm3.Mesurable {
  public static final int LESS;

  public static final int EQUAL;

  public static final int GREATER;

  public abstract int compareTo(java.lang.Object);
}

注:一个编译后的java类不包含package和import段,因此在class文件中所有的类型都使用的是全路径。

4.4 ASMifier

可能有人会问,我刚开始学,上面例子中那些 ASM 的代码我还不会写,怎么办呢?ASM 官方为我们提供了 ASMifier,可以帮助我们生成这些晦涩难懂的 ASM 代码。

比如,我想通过 ASM 实现统计一个方法的执行时间,该怎么做呢?一般会有如下的代码:

package com.lijiankun24.classpractice;

public class Demo {

    public void costTime() {
        long startTime = System.currentTimeMillis();
        // ......
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("The cost time of this method is " + duration + " ms");
    }
}

那上面这段代码对应的 ASM 代码是什么呢?我们可以通过以下两个步骤,使用 ASMifier 自动生成:

  1. 通过 javac 编译该 Demo.java 文件生成对应的 Demo.class 文件,如下所示
javac Demo.java
  1. 通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,然后使用如下命令,即可生成
java -classpath asm-all-5.2.jar org.objectweb.asm.util.ASMifier Demo.class

截图如下:

在这里插入图片描述

使用 ASM 进行 AOP 编程

改造 Account 类

接下来,我们要使用 ASM 给 Account 类加上 security check 的功能。与 proxy 编程不同,ASM 不需要将Account 声明成接口,Account 可以仍旧是一个实现类。ASM 将直接在 Account 类上动手术,给 Account 类的operation 方法首部加上对 SecurityChecker.checkSecurity 的调用。

首先,我们将从 ClassAdapter 继承一个类。ClassAdapter 是 ASM 框架提供的一个默认类,负责沟通ClassReaderClassWriter。如果想要改变 ClassReader 处读入的类,然后从 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);
		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");
	}
}   

其中,ClassReader 读到每个方法的首部时调用 visitCode(),在这个重写方法里,我们用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V"); 插入了安全检查功能。

最后,我们将集成上面定义的 ClassAdapterClassReaderClassWriter 产生修改后的 Account 类文件:

import java.io.File;
import java.io.FileOutputStream;
import org.objectweb.asm.*;

public class Generator{
	public static void main() throws Exception {
		ClassReader cr = new ClassReader("Account");
		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
		ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
		cr.accept(classAdapter, ClassReader.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 类本身的,从此 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) {            
			ClassReader cr = new ClassReader("Account");
			ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
			ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
			cr.accept(classAdapter, ClassReader.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());
		}
	}
}

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值