阅读 2421

教你用Java字节码做点有趣的事(二)之ASM

0.写在前面

本篇是本系列的第二篇,主要介绍什么是ASM,以及如何使用ASM。 如果没有阅读之前的教你用Java字节码做点有趣的事,还请阅读一下,因为需要上一章的部分需求。

1.什么是ASM

在上节我们知道,通过javac编译生成之后生成的是字节码,但是我们可能会有一些需求,比如需要AOP切面,事务的统一管理,有些重复的代码需要我们来回的敲,又或者我们需要生成自己的字节码来使用(fastjson就是这么做的)。但是字节码如果我们直接操作,成本太大,并且效率也不高。这个时候你就需要一款利器,将字节码转换成java语言,从而你就可以随心所欲的操纵字节码。这些工具如ASM,例如Javaassit,BCEL等等,都可以用来操作字节码。 而这里我要介绍的就是操作字节码的一把利器-ASM,ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 ASM的优点如下:

  • 有一个简单的模块API,设计完善,使用方便。
  • 更新速度快,支持最新的Java版本
  • 小而快,非常可靠
  • 已经有很多著名的开源框架都在使用,例如cglib,spring,fastjson等等。
  • 源许可开放,几乎允许任意使用。 asm的具体流程如下所示:

2.ASM的简单入门

在这个小标题我会简单的介绍,如何去使用ASM。在这里之前我希望你有idea编译器,如果你有的话,可以去插件库里面下载一个ASM Bytecode Outline。有了这个我们后面开发ASM将会感受到美滋滋,如鱼得水。

ASM 库􁨀供了两个用于生成和转换已编译类的API,一个是核心API,以基于事件的形式来表示类,另一个是树API,以基于对象的形式来表示类。

  • 核心api,可以对比XML中解析的SAX,不需要把这个类的整个结构读取进来,节约内存,但是编程难度较大。在采用基于事件的模型时,类是用一系列事件来表示的,每个事件表示类的一个元素,比如它的一个字段、一个方法声明、一条指令,等等。基于事件的API定义了一组可能事件,以及这些事件必须遵循的发生顺序,还􁨀供了一个类分析器,为每个被分析元素生成一个事件,还􁨀供一个类写入器,由这些事件的序列生成经过编译的类。
  • 树API,对比XML解析中的DOM,需要把整个类的结构读取到内存中,消耗内存多,但是变成较为简单

没有asm jar包的同学需要引入下面的maven:

        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>5.0.4</version>
        </dependency>
复制代码

2.1核心api

这里大家是否已经下好了那个插件(ASM Bytecode Outline)呢?如果下载完毕,还记得我们上一节的那个例子吗?

public class ByteCodeDemo {
    private static final String name = "xiaoming";

    private int age;

    public ByteCodeDemo(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
    public void setAge(){
        this.age = age;
    }
    public static void main(String[] args) {
        ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12);
        System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge());
    }
}
复制代码

在这个类中,右键点击下方

然后右方会生成我们的如果用ASM生成这个类,那么应该是哪些代码:

复制旁边的代码,你就能生成你的class的二进制文件。

如果你看不懂,没关系,我这里会慢慢的讲。

2.1.1 核心类

在ASM的core API编程中有几个关键类:

  • ClassReader:可以读取编译好的二进制Class文件
  • ClassWriter:用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
  • ClassVisitor:用于生成和变转已编译类。在ClassVisitor定义了很多方法,例如:类上的注解,类的构造方法,类的字段,类的方法,静态代码块访问。用于我们去重写,以便做一些类上的逻辑扩展。要注意的是ClassWriter继承的是ClassVistor,这里ClassWriter就可以边访问边写入。
  • 上面说了ClassVisitor,这里再说说其他的Visitor,ASM core api里的代码是根据字节码从上到下依次生成,可以看见里面还有一些其他的Visitor,比如MethodVisitor,用于访问Method,如果重写其部分方法可以对方法进行修改。FieldVisitor,用于访问类的变量,常量,如果重写其部分方法可以进行修改。AnnotationVisitor,用于访问的注解。

visitor是我们扩展我们类自己码的关键

2.2 coreApi如何编写代码

 public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("java/java8/ByteCodeDemo.class");
        ClassReader classReader = new ClassReader(fileInputStream);
        ClassWriter cw = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        //Java8选择ASM5,
        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                System.out.println("field:" + name);
                return super.visitField(access, name, desc, signature, value);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                System.out.println("方法" + name);
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
        };
        //忽略调试信息
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

    }
复制代码

我们下面输出:

field:name
field:age
方法<init>
方法getAge
方法setAge
方法main
复制代码

可以看到我们已经通过visitField和visitMethod,进行对每个field和每个Method的名字都进行了输出,其中方法包括了编译器帮我们创建的构造方法和我们自定义的三个方法。

3.树形API

由于后面的代码都会通过coreApi来做,这里树形API简单用例子说明一下:

public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("/Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class");
        ClassReader classReader = new ClassReader(fileInputStream);
        ClassWriter cw = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        //忽略调试信息
        ClassNode classNode = new ClassNode(org.objectweb.asm.Opcodes.ASM5);


        classReader.accept(classNode, ClassReader.SKIP_DEBUG);
        for (MethodNode methodNode:classNode.methods) {
            System.out.println(methodNode.name);
        }
        classNode.accept(cw);
    }
复制代码

输出如下:

<init>
getAge
setAge
main
复制代码

4.最后

本文简单介绍了ASM的介绍,以及简单的使用,下一章会介绍如何去用ASM做上一篇介绍的小工具,以及java的instrument机制。 同时想获取ASM更多高级用法可以关注我的公众号回复asm即可获取。

由于水平不足,如有错误还请批评与指正!

为了方便大家学习交流,建了个qq java后端交流群:837321192,里面有我收藏的百G学习视频(涵盖面试,架构等等),也有很多面试资料,可以加入进来一起交流。

如果大家觉得这篇文章对你有帮助,或者想提前获取后续章节文章,或者你有什么疑问想提供1v1免费vip服务,都可以关注我的公众号,关注即可免费领取上百G最新java学习资料视频,以及最新面试资料,你的关注和转发是对我最大的支持,O(∩_∩)O:

关注下面的标签,发现更多相似文章
评论