前面几篇文章介绍了 .class 文件的结构、JVM 如何加载 .class 文件、JVM 中如何执行方法的调用和访问者模式,其实前面几篇文章都是为这篇文章做铺垫的,如果不知道 .class 文件结构、也不知道在 JVM 中 .class 文件中的方法是如何被执行的,这篇文章中的有些部分可能会看不懂,所以推荐先看下前面几篇文章。
这篇文章主要介绍 ASM 库的结构、主要的 API,并且通过两个示例说明如何通过 ASM 修改 .class 文件中的方法和属性。
catalog.png
一. ASM 的结构
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
ASM 库的结构如下所示:
asm_arch.png
- Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和 ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类.
- Tree:提供了 Java 字节码在内存中的表现
- Commons:提供了一些常用的简化字节码生成、转换的类和适配器
- Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用
- XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化
二. Core API 介绍
2.1 ClassVisitor 抽象类
如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
1 | public abstract class ClassVisitor { |
- void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口) - AnnotationVisitor visitAnnotation(String desc, boolean visible)
该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。 - FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值) - MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常) - void visitEnd()
该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法
2.2 ClassReader 类
这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
2.3 ClassWriter 类
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
2.4 MethodVisitor & AdviceAdapter
MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。
AdviceAdapter 的方法如下所示:
AdviceAdapter.png
其中比较重要的几个方法如下:
- void visitCode():表示 ASM 开始扫描这个方法
- void onMethodEnter():进入这个方法
- void onMethodExit():即将从这个方法出去
- void onVisitEnd():表示方法扫码完毕
2.5 FieldVisitor 抽象类
FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍
2.6 操作流程
- 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
- 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
- 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
三. 示例
3.1 修改类中方法的字节码
假如现在我们有一个 HelloWorld 类,如下
1 | package com.lijiankun24.asmpractice.demo; |
通过 javac HelloWorld.java
和 javap -verbose HelloWorld.class
可以查看到 sayName() 方法的字节码如下所示:
1 | public void sayHello(); |
我们通过 ASM 修改 HelloWorld.class 字节码文件,实现统计方法执行时间的功能
1 | public class CostTime { |
执行结果如下图所示
Class.png
反编译 HelloWorld2.class 文件的内容如下所示
Class1.png
3.2 修改类中属性的字节码
这一节中我们将展示一下如何使用 Core API 对类中的属性进行操作。
假如说,现在有一个 Person.java 类如下所示:
1 | public class Person { |
我们想为这个类,添加一个 ‘public int age’ 的属性该怎么添加呢?我们会面对两个问题:
- 该调用 ASM 的哪个 API 添加属性呢?
- 在何时写添加属性的代码?
接下来,我们就一一解决上面的两个问题?
3.2.1 添加属性的 API
按照我们分析的上述的 2.6 操作流程叙述,需要以下三个步骤:
- 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
- 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
- 需要创建一个事件过滤器 ClassVisitor。事件过滤器中的某些方法可以产生一个新的XXXVisitor对象,当我们需要修改对应的内容时只要实现自己的XXXVisitor并返回就可以了
在上面三个步骤中,可以操作的就是 ClassVisitor 了。ClassVisitor 接口提供了和类结构同名的一些方法,这些方法可以对相应的类结构进行操作。
在使用 ClassVisitor 添加类属性的时候,只需要添加一句话就可以了:
1 | classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null); |
visitField.png
3.2.2 添加属性的时机
我们先暂且在 ClassVisitor 的 visitEnd() 方法中写入上面的代码,如下所示
1 | public class Transform extends ClassVisitor { |
我们写如下的测试类,测试一下
1 | public class FieldPractice { |
其输出入下所示:
visitFieldResult.png
那如果我们尝试在 ClassVisitor#visitField() 方法中添加属性可以吗?我们可以修改 Transform 测试一下:
1 | public class Transform extends ClassVisitor { |
还是使用上面的测试代码测试一下,会有如下的测试结果
visitFieldError.png
在 Person 类中有重复的属性,为什么会报这个错误呢?
分析 ClassVisitor#visitField() 方法可得知,只要访问类中的一个属性,visitField() 方法就会被调用一次,在 Person 类中有两个属性,所以 visitField() 方法就会被调用两次,也就添加了两次 ‘public int age’ 属性,就报了上述的错误,而 visitEnd() 方法只有在最后才会被调用且只调用一次,所以在 visitEnd() 方法中是添加属性的最佳时机
3.3 ASMifier
可能有人会问,我刚开始学,上面例子中那些 ASM 的代码我还不会写,怎么办呢?ASM 官方为我们提供了 ASMifier,可以帮助我们生成这些晦涩难懂的 ASM 代码。
比如,我想通过 ASM 实现统计一个方法的执行时间,该怎么做呢?一般会有如下的代码:
1 | package com.lijiankun24.classpractice; |
那上面这段代码对应的 ASM 代码是什么呢?我们可以通过以下两个步骤,使用 ASMifier 自动生成:
- 通过
javac
编译该Demo.java
文件生成对应的Demo.class
文件,如下所示
1 | javac Demo.java |
- 通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载
asm-all.jar
库,我下载的是最新的asm-all-5.2.jar
,然后使用如下命令,即可生成
1 | java -classpath asm-all-5.2.jar org.objectweb.asm.util.ASMifier Demo.class |
截图如下:
DemoDump.png
深入字节码 – 玩转 ASM-Bytecode 原 荐
美团热更方案ASM实践
43人点赞
作者:lijiankun24
链接:https://www.jianshu.com/p/905be2a9a700
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Author: boybeak