在当前阶段,我们只能进行 Class Generation 的操作。
示例一:生成接口
预期目标
我们的预期目标:生成 HelloWorld
接口。
public interface HelloWorld {
}
编码实现
import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import static org.objectweb.asm.Opcodes.*;
public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
// (1) 生成 byte[] 内容
byte[] bytes = dump();
// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}
public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用 visitXxx() 方法
cw.visit(
V1_8, // version
ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, // access
"sample/HelloWorld", // name
null, // signature
"java/lang/Object", // superName
null // interfaces
);
cw.visitEnd();
// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}
在上述代码中,我们调用了 visit()
方法、visitEnd()
方法和 toByteArray()
方法。
由于 sample.HelloWorld
这个接口中,并没有定义任何的字段和方法,因此,在上述代码中没有调用 visitField()
方法和 visitMethod()
方法。
验证结果
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sample.HelloWorld");
System.out.println(clazz);
}
}
小总结
visit() 方法
在这里,我们重点介绍一下 visit(version, access, name, signature, superName, interfaces)
方法的各个参数:
version
: 表示当前类的版本信息。在上述示例代码中,其取值为Opcodes.V1_8
,表示使用 Java 8 版本。access
: 表示当前类的访问标识(access flag)信息。在上面的示例中,access
的取值是ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE
,也可以写成ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE
。如果想进一步了解这些标识的含义,可以参考 Java Virtual Machine Specification 的 Chapter 4. The class File Format 部分。name
: 表示当前类的名字,它采用的格式是 Internal Name 的形式。signature
: 表示当前类的泛型信息。因为在这个接口当中不包含任何的泛型信息,因此它的值为null
。superName
: 表示当前类的父类信息,它采用的格式是 Internal Name 的形式。interfaces
: 表示当前类实现了哪些接口信息。
Internal Name
同时,我们也要介绍一下 Internal Name 的概念:在 .java
文件中,我们使用 Java 语言来编写代码,使用类名的形式是Fully Qualified Class Name,例如 java.lang.String
;将 .java
文件编译之后,就会生成 .class
文件;在 .class
文件中,类名的形式会发生变化,称之为Internal Name,例如 java/lang/String
。因此,将Fully Qualified Class Name转换成Internal Name的方式就是,将 .
字符转换成 /
字符。
Java Language | Java ClassFile | |
---|---|---|
文件格式 | .java |
.class |
类名 | Fully Qualified Class Name | Internal Name |
类名示例 | java.lang.String |
java/lang/String |
示例二:生成接口 + 字段 + 方法
预期目标
我们的预期目标:生成 HelloWorld
接口。
public interface HelloWorld extends Cloneable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}
编码实现
import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
// (1) 生成 byte[] 内容
byte[] bytes = dump();
// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}
public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "sample/HelloWorld",
null, "java/lang/Object", new String[]{"java/lang/Cloneable"});
{
FieldVisitor fv1 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, -1);
fv1.visitEnd();
}
{
FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, 0);
fv2.visitEnd();
}
{
FieldVisitor fv3 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, 1);
fv3.visitEnd();
}
{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null);
mv1.visitEnd();
}
cw.visitEnd();
// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}
在上述代码中,我们调用了 visit()
方法、visitField()
方法、visitMethod()
方法、visitEnd()
方法和 toByteArray()
方法。
验证结果
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sample.HelloWorld");
Field[] declaredFields = clazz.getDeclaredFields();
if (declaredFields.length > 0) {
System.out.println("fields:");
for (Field f : declaredFields) {
System.out.println(" " + f.getName());
}
}
Method[] declaredMethods = clazz.getDeclaredMethods();
if (declaredMethods.length > 0) {
System.out.println("methods:");
for (Method m : declaredMethods) {
System.out.println(" " + m.getName());
}
}
}
}
输出结果:
fields:
LESS
EQUAL
GREATER
methods:
compareTo
小总结
visitField() 和 visitMethod() 方法
在这里,我们重点说一下 visitField()
方法和 visitMethod()
方法的各个参数:
visitField (access, name, descriptor, signature, value)
visitMethod(access, name, descriptor, signature, exceptions)
这两个方法的前 4 个参数是相同的,不同的地方只在于第 5 个参数。
access
参数:表示当前字段或方法带有的访问标识(access flag)信息,例如ACC_PUBLIC
、ACC_STATIC
和ACC_FINAL
等。name
参数:表示当前字段或方法的名字。descriptor
参数:表示当前字段或方法的描述符。这些描述符,与我们平时使用的 Java 类型是有区别的。signature
参数:表示当前字段或方法是否带有泛型信息。换句话说,如果不带有泛型信息,提供一个null
就可以了;如果带有泛型信息,就需要给它提供某一个具体的值。value
参数:是visitField()
方法的第 5 个参数。这个参数的取值,与当前字段是否为常量有关系。如果当前字段是一个常量,就需要给value
参数提供某一个具体的值;如果当前字段不是常量,那么使用null
就可以了。exceptions
参数:是visitMethod()
方法的第 5 个参数。这个参数的取值,与当前方法头(Method Header)中是否具有throws XxxException
相关。
我们可以使用 PrintASMCodeCore
类来查看下面的 sample.HelloWorld
类的 ASM 代码,从而观察 value
参数和 exceptions
参数的取值:
import java.io.FileNotFoundException;
import java.io.IOException;
public class HelloWorld {
// 这是一个常量字段,使用 static、final 关键字修饰
public static final int constant_field = 10;
// 这是一个非常量字段
public int non_constant_field;
public void test() throws FileNotFoundException, IOException {
// do nothing
}
}
对于上面的代码,
constant_field
字段:对应于visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "constant_field", "I", null, new Integer(10))
non_constant_field
字段:对应于visitField(ACC_PUBLIC, "non_constant_field", "I", null, null)
test()
方法:对应于visitMethod(ACC_PUBLIC, "test", "()V", null, new String[] { "java/io/FileNotFoundException", "java/io/IOException" })
描述符(descriptor)
在 ClassFile 当中,描述符(descriptor)是对“类型”的简单化描述。
- 对于字段(field)来说,描述符(descriptor)就是对字段本身的类型进行简单化描述。
- 对于方法(method)来说,描述符(descriptor)就是对方法的接收参数的类型和返回值的类型进行简单化描述。
Java 类型 | ClassFile 描述符 |
---|---|
boolean |
Z (Z 表示 Zero,零表示 `false`,非零表示 `true`) |
byte |
B |
char |
C |
double |
D |
float |
F |
int |
I |
long |
J |
short |
S |
void |
V |
non-array reference |
L<InternalName>; |
array reference |
[ |
对字段描述符的举例:
boolean flag
:Z
byte byteValue
:B
int intValue
:I
float floatValue
:F
double doubleValue
:D
String strValue
:Ljava/lang/String;
Object objValue
:Ljava/lang/Object;
byte[] bytes
:[B
String[] array
:[Ljava/lang/String;
Object[][] twoDimArray
:[[Ljava/lang/Object;
对方法描述符的举例:
int add(int a, int b)
:(II)I
void test(int a, int b)
:(II)V
boolean compare(Object obj)
:(Ljava/lang/Object;)Z
void main(String[] args)
:([Ljava/lang/String;)V
示例三:生成类
预期目标
我们的预期目标:生成 HelloWorld
类。
public class HelloWorld {
}
编码实现
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
import static org.objectweb.asm.Opcodes.*;
public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
// (1) 生成 byte[] 内容
byte[] bytes = dump();
// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}
public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
cw.visitEnd();
// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}
验证结果
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sample.HelloWorld");
System.out.println(clazz);
}
}
<init>() 和 <clinit>() 方法
对于一个类(Class)来说,如果没有提供任何构造方法,Java 编译器会自动生成一个默认构造方法。在所有的 .class
文件中,构造方法的名字是 <init>()
。
另外,如果在 .class
文件中包含静态代码块,那么就会有一个 <clinit>()
方法。
package sample;
public class HelloWorld {
static {
System.out.println("static code block");
}
}
上面的静态代码码,对应于 visitMethod(ACC_STATIC, "<clinit>", "()V", null, null)
的调用。
总结
本文主要对 ClassWriter
类的代码示例进行介绍,主要目的是希望大家能够对 ClassWriter
类熟悉起来。
本文内容总结如下:
- 第一点,我们需要注意
ClassWriter
/ClassVisitor
中visit()
、visitField()
、visitMethod()
和visitEnd()
方法的调用顺序。 - 第二点,我们对于
visit()
方法、visitField()
方法和visitMethod()
方法接收的参数进行了介绍。虽然我们并没有特别介绍visitEnd()
方法和toByteArray()
方法,并不表示这两个方法不重要,只是因为这两个方法不接收任何参数。 - 第三点,我们介绍了 Internal Name 和 Descriptor(描述符)这两个概念,在使用时候需要加以注意,因为它们与我们在使用 Java 语言编写代码时是不一样的。
- 第四点,在
.class
文件中,构造方法的名字是<init>()
,表示 instance initialization method;静态代码块的名字是<clinit>()
,表示 class initialization method。
另外,visitField()
方法会返回一个 FieldVisitor
对象,而 visitMethod()
方法会返回一个 MethodVisitor
对象;在后续的内容当中,我们会分别介绍 FieldVisitor
类和 MethodVisitor
类。