【Javassist官方文档翻译】第5-10章

系列文章目录

第一章 读写字节码
第二章 类池
第三章 类加载器
第四章 类加载器


前言

在上一章我们介绍了Javassist的常见标识符、更改方法体以及新增或删除方法和字段等。本篇文章将介绍Javassist官方文档的最后五个章节的内容。


5.字节码级API

Javassist还提供了用于直接编辑类文件的低级API。要使用这一级别的API,您需要详细了解Java字节码和类文件格式,这一级别的API允许您对类文件进行任何修改。

如果您只想生成一个简单的类文件,javassist.bytecode.ClassFileWriter对您来说是最合适的api。尽管它的API是最小的,它比javassist.bytecode.ClassFile的api还要快得多。

5.1获取ClassFile对象

javassist.bytecode.ClassFile对象表示类文件。要获取此对象,应调用CtClass中的getClassFile方法。

否则,您可以直接从类文件构造一个javassist.bytecode.ClassFile。例如

BufferedInputStream fin
    = new BufferedInputStream(new FileInputStream("Point.class"));
ClassFile cf = new ClassFile(new DataInputStream(fin));

这段代码从Point.class字节码文件创建ClassFile对象。

ClassFile对象可以写回类文件。write方法将类文件的内容写入指定的DataOutputStream。

您可以从头开始创建新的类文件。例如:

ClassFile cf = new ClassFile(false, "test.Foo", null);
cf.setInterfaces(new String[] { "java.lang.Cloneable" });
 
FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));

该代码生成一个类文件Foo。类,该类包含以下类的实现:

package test;
class Foo implements Cloneable {
    public int width;
}

5.2 添加和删除成员

ClassFile提供addField方法和addMethod方法来添加字段或方法(注意,构造函数被在字节码级别中被视为方法)。它还提供了addAttribute方法,用于向类文件添加属性。

请注意,FieldInfo、MethodInfo和AttributeInfo对象包含指向ConstPool(常量池表)对象的链接。ConstPool对象必须与ClassFile对象和添加到该ClassFile对象的FieldInfo(或MethodInfo等)对象共用。换句话说,FieldInfo(或MethodInfo等)对象不能在不同的ClassFile对象之间共享。

要从ClassFile对象中删除字段或方法,必须首先获得包含类的所有字段的java.util.List 对象。getFields方法和getMethods方法返回这个列表对象。可以通过对列表对象调用remove方法来删除字段或方法。属性也可以用类似的方式删除。在FieldInfo或MethodInfo中调用getAttributes方来获取属性列表,并从列表中删除一个。

5.3遍历方法体

要检查方法体中的每个字节码指令,CodeIterator非常有用。要获取此对象,请执行以下操作:

ClassFile cf = ... ;
MethodInfo minfo = cf.getMethod("move");    // 我们假设move方法没有重载。
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator i = ca.iterator();

CodeIterator对象允许您从头到尾逐个访问每个字节码指令。以下是CodeIterator中声明的部分方法:

  • void begin() 移至第一条指令。
  • void move(int index) 移动到指定索引的指令。
  • boolean hasNext() 如果有更多指令,则返回true。
  • int next() 返回下一条指令的索引。 注意,它不会返回下一条指令的操作码。
  • int byteAt(int index) 返回索引处的无符号8位值。
  • int u16bitAt(int index) 返回索引处的无符号16位值。
  • int write(byte[] code, int index) 在索引处写入字节数组。
  • void insert(int index, byte[] code) 在索引处插入字节数组。分支偏移等自动调整。

以下代码段展示了方法体中包含的所有指令:

CodeIterator ci = ... ;
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    System.out.println(Mnemonic.OPCODE[op]);
}

5.4 生成字节码序列

Bytecode对象表示字节码指令序列。它是一个可增长的字节码数组。以下代码是一个示例:

ConstPool cp = ...;    // 常量池表
Bytecode b = new Bytecode(cp, 1, 0);
b.addIconst(3);
b.addReturn(CtClass.intType);
CodeAttribute ca = b.toCodeAttribute();

这将生成表示以下序列的代码属性:

iconst_3
ireturn

您还可以通过调用Bytecode中get方法来获取包含此序列的字节数组。可以将获得的数组插入到另一个代码属性中。

Bytecode不仅提供了许多向序列中添加特定指令的方法,还提供了用于添加8位操作码的addOpcode方法和用于添加索引的addIndex方法。每个操作码的8位值都在opcode接口中定义。

除非控制流不包含分支,否则addOpcode方法和其他用于添加特定指令的方法将自动保持最大堆栈深度。该值可以调用Bytecode对象的getMaxStack方法获得。它也反映在由Bytecode对象构造的CodeAttribute对象上。若要重新计算方法体的最大堆栈深度,请调用CodeAttribute中的computeMaxStack方法。

Bytecode 可用于构造方法。例如

ClassFile cf = ...
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);
code.setMaxLocals(1);

MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);

此代码生成默认构造函数,并将其添加到cf指定的类中。Bytecode对象首先转换为CodeAttribute对象,然后添加到minfo指定的方法中。该方法最终被添加到类文件cf中。

5.5 注解(元标记)

注解作为运行时不可见(或可见)的属性存储在类文件中。这些属性可以从ClassFile、MethodInfo或FieldInfo对象中调用getAttribute(AnnotationsAttribute.invisibleTag)方法获得。有关更多详细信息,请参阅javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包。

Javassist还允许您通过高级API访问注解。如果要通过CtClass访问注解,在CtClass或CtBehavior中调用getAnnotations方法即可。

6.泛型

Javassist的低级API完全支持Java5引入的泛型。另一方面,CtClass等高级API不直接支持泛型。然而,这对于字节码转换来说并不是一个严重的问题。

Java的泛型是通过擦除技术实现的。编译后,所有类型参数都会被删除。例如,假设源代码声明了一个参数化类型Vector:

Vector<String> v = new Vector<String>();
  :
String s = v.get(0);

编译的字节码相当于以下代码:

Vector v = new Vector();
  :
String s = (String)v.get(0);

因此,当您编写字节码转换器时,您可以删除所有类型参数。由于Javassist中嵌入的编译器不支持泛型,因此如果源代码是由Javassist编译的,例如,通过CtMethod.make(),则必须在调用方站点插入显式类型转换。如果源代码由普通Java编译器(如javac)编译,则不需要类型转换。

例如,如果您有一个类:

public class Wrapper<T> {
  T value;
  public Wrapper(T t) { value = t; }
}

并希望将接口Getter添加到类Wrapper:

public interface Getter<T> {
  T get();
}

那么您真正需要添加的接口是Getter(类型参数会丢弃),您需要添加到Wrapper类的方法是这样的一个简单方法:

public Object get() { return value; }

请注意,不需要类型参数。由于get方法返回Object,如果源代码是由Javassist编译的,则需要在调用方进行显式类型转换。例如,如果类型参数T为String,则必须按如下方式插入(String):

Wrapper w = ...
String s = (String)w.get();

如果源代码由普通Java编译器编译,则不需要类型转换,因为编译器会自动插入类型转换。

如果需要在运行时通过反射访问类型参数,则必须向类文件添加泛型签名。有关更多详细信息,请参阅CtClass中setGenericSignature方法的API文档(javadoc)。

7.可变参数

目前,Javassist不直接支持可变参数。因此,要使用varargs创建方法,必须显式设置方法修饰符。但这很容易。假设现在要使用以下方法:

public int length(int... args) { return args.length; }

使用Javassist的以下代码将生成上述方法:

CtClass cc = /* 目标类 */;
CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc);
m.setModifiers(m.getModifiers() | Modifier.VARARGS);
cc.addMethod(m);

参数类型int…更改为int[]和Modifier。VARARGS被添加到方法修饰符中。

要在Javassist中嵌入的编译器编译的源代码中调用此方法,必须编写:

length(new int[] { 1, 2, 3 });

而不是使用varargs机制进行此方法调用:

length(1, 2, 3);

8. J2ME

如果修改J2ME执行环境的类文件,则必须执行预验证。预验证基本上是生成堆栈映射,这类似于JDK1.6中引入J2SE的堆栈映射表。Javassist仅在avassist.bytecode.MethodInfo.doPreverify为真时维护堆栈映射。

还可以为修改的方法手动生成堆栈映射。对于CtMethod对象m表示的给定方法,可以通过调用以下方法生成堆栈映射:

m.getMethodInfo().rebuildStackMapForME(cpool);

这里,cpool是一个ClassPool对象,可以通过调用CtClass对象中的getClassPool方法来获得。ClassPool对象负责从给定的类路径中查找类文件。要获取所有CtMethod对象,调用CtClass对象的getDeclaredMethods方法即可。

9.装箱/拆箱

Java中的装箱和拆箱是语法糖。没有用于装箱或拆箱的字节码。所以Javassist的编译器不支持它们。例如,以下语句在Java中有效:

Integer i = 3;

因为装箱是隐式执行的。但是,对于Javassist,必须将值类型从int显式转换为Integer:

Integer i = new Integer(3);

10. Debug

设置CtClass.debugDump为目录名。然后,Javassist修改和生成的所有类文件都保存在该目录中。要停止此操作,请设置CtClass.debugDump为null。默认值为null。

例如:


CtClass.debugDump = "./dump";

所有被修改的类文件都保存在./dump中。

总结

本篇文章介绍了Javassist的字节码级的api,以及javassist的一些其他特性。至此2022年12月18日,Javassist官网教程已翻译完成。Javassist中文技术资料过于匮乏,所以博主才有了翻译Javassist官方教程的想法。后续会准备一些Javassist在实际项目中使用案例。进一步了解Javassist框架。

说明

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值