用于生成和转换已编译 Java 类的 ASM 树 API 是基于 ClassNode 类的(见图 6.1)。
图 6.1 ClassNode 类(仅给出了字段)
public class ClassNode ...{
public int version;
public int access;
public String name;
public String signature;
public String superName;
public List<String> interfaces;
public String sourceFile;
public String sourceDebug;
public String outerClass;
public String outerMethod;
public String outerMethodDesc;
public List<AnnotationNode> visibleAnnotations;
public List<AnnotationNode> invisibleAnnotations;
public List<Attribute> attrs;
public List<InnerClassNode> innerClasses;
public List<FieldNode> fields;
}
可以看出,这个类的公共字段对应于图 2.1 中给出的类文件结构部分。这些字段的内容与核心 API 相同。例如,name 是一个内部名字,signature 是一个类签名(见 2.1.2 节和 4.1 节)。一些字段包含其他 XxxNode 类:这些类将在随后各章详细介绍,它们拥有一种类似的结构,即拥有一些字段,对应于类文件结构的子部分。例如,FieldNode 类看起来是这样的:
public class FieldNode ...{
public int access;
public String name;
public String desc;
public String signature;
public Object value;
public FieldNode(int access,String name,String desc,String signature,Object value){
...
}
...
}
MethodNode 类是类似的:
public class MethodNode ... {
public int access;
public String name;
public String desc;
public String signature;
public List<String> exceptions;
...
public MethodNode(int access, String name, String desc, String signature, String[] exceptions){
...
}
}
用树 API 生成类的过程就是:创建一个 ClassNode 对象,并初始化它的字段。例如,2.2.3 节的 Comarable 接口可用如下代码生成(其代码数量大体与 2.2.3 节相同):
ClassNode cn = new ClassNode();
cn.version = V1_5;
cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
cn.name = "pkg/Comparable"; cn.superName = "java/lang/Object"; cn.interfaces.add("pkg/Mesurable");
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)));
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null));
使用树 API 生成类时,需要多花费大约 30%的时间(见附录 A.1),占用的内存也多于使用核心 API。但可以按任意顺序生成类元素,这在一些情况下可能非常方便。
添加和删除类就是在 ClassNode 对象的 fields 或 methods 列表中添加或删除元素。例如,如果像下面这样定义了 ClassTransformer 类,以便能够轻松地编写类转换器:
public class ClassTransformer {
protected ClassTransformer ct;
public ClassTransformer(ClassTransformer ct) {
this.ct = ct;
}
public void transform(ClassNode cn) {
if (ct != null) {
ct.transform(cn);
}
}
}
则 2.2.5 节中的 RemoveMethodAdapter 可实现如下:
public class RemoveMethodTransformer extends ClassTransformer {
private String methodName;
private String methodDesc;
public RemoveMethodTransformer(ClassTransformer ct, String methodName, String methodDesc) {
super(ct);
this.methodName = methodName;
this.methodDesc = methodDesc;
}
@Override
public void transform(ClassNode cn) {
Iterator<MethodNode> i = cn.methods.iterator();
while (i.hasNext()) {
MethodNode mn = i.next();
if (methodName.equals(mn.name) && methodDesc.equals(mn.desc)) {
i.remove();
}
}
super.transform(cn);
}
}
可以看出,它与核心 API 的主要区别是需要迭代所有方法,而在使用核心 API 时是不需要这样做的(这一工作会在 ClassReader 中为你完成)。事实上,这一区别对于几乎所有基于树的转换都是有效的。例如,在用树 API 实现 2.2.6 节的 AddFieldAdapter 时,它还需要一个迭代器:
public class AddFieldTransformer extends ClassTransformer {
private int fieldAccess;
private String fieldName;
private String fieldDesc;
public AddFieldTransformer(ClassTransformer ct, int fieldAccess, String fieldName, String fieldDesc) {
super(ct);
this.fieldAccess = fieldAccess;
this.fieldName = fieldName;
this.fieldDesc = fieldDesc;
}
@Override
public void transform(ClassNode cn) {
boolean isPresent = false;
for (FieldNode fn : cn.fields) {
if (fieldName.equals(fn.name)) {
isPresent = true;
break;
}
}
if (!isPresent) {
cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc,
null, null));
}
super.transform(cn);
}
}
和生成类的情景一样,使用树 API 转换类时,所花费的时间和占用的内存也要多于使用核心 API 的时候。但使用树 API 有可能使一些转换的实现更为容易。比如有一个转换,要向一个类中添加注释,包含其内容的数字签名,就属于上述情景。在使用核心 API 时,只有在访问了整个类之后才能计算数字签名,但这时再添加包含其内容的注释就太晚了,因为对注释的访问必须位于类成员之前。而在使用树 API 时,这个问题就消失了,因为这时不存在此种限制。
事实上,有可能用核心 API 实现 AddDigitialSignature 示例,但随后,必须分两遍来转换这个类。第一遍,首先用一个 ClassReader(没有 ClassWriter)来访问这个类,以根据类的内容来计算数字签名。在第二遍,重复利用同一个 ClassReader 对类进行第一次访问, 这一次是向一个 ClassWriter 链接一个 AddAnnotationAdapter。通过推广这一论述过程, 我们可以看出,事实上,任何转换都可以仅用核心 API 来实现,只需在必要时分几遍完成。但这样就提高了转换代码的复杂性,要求在各遍之间存储状态(这种状态可能非常复杂,需要一个完整的树形表示!),而且对一个类进行多次分析是有成本的,必需将这一成本与构造相应 ClassNode 的成本进行比较。
结论是:树 API 通常用于那些不能由核心 API 一次实现的转换。但当然也存在例外。例如一个混淆器不能由核心 API 一遍实现,因为必须首先在原名称和混淆后的名字之间建立了完整的映射之后,才可能转换类,而这个映射的建立需要对所有类进行分析。但树 API 也不是一个好的解决方案,因为它需要将所有待混淆类的对象表示保存在内存中。在这种情况下,最好是分两遍使用核心 API:一遍用于计算原名与混淆后名称之间的映射(一个简单的散列表,它需要的内存要远少于所有类的完整对象表示),另一遍用于根据这一映射来转换类。