欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ASM 系列详细教程-16-ASM Tree api 类接口与组件

程序员文章站 2022-06-17 14:15:35
...

本章简介

本章介绍如何使用ASM树API生成和转换类。

它从仅介绍tree API入手,然后进行说明如何与核心API组合在一起。

在下一章中将说明用于方法,注解和泛型内容的树形API。

Interfaces and components

Presentation(介绍)

类节点信息

用于生成和转换已编译Java类的ASM树API基于ClassNode类(请参见图6.1)。

  • Figure 6.1.: The ClassNode class (only fields are shown)
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;
    public List<MethodNode> methods;
}

如您所见,该类的公共字段与图2.1中显示的类文件结构部分相对应。

这些字段的内容与核心API中的内容相同。

ps: 这里是 asm 的两种访问模式,所以信息是一一对应的。

例如,实例名称是内部名称,签名是类签名(请参见2.1.2和4.1节)。

一些字段包含其他Xxx Node类:这些类将在下一章中详细介绍,它们具有类似的结构,即具有与类文件结构的子部分相对应的字段。

字段节点

例如,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) {
    ...
    }
    ...
}

方法节点

类似

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节中的Comparable接口,并使用与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相比,使用树API生成类要花费大约30%的时间(请参阅附录A.1),并消耗更多的内存。

但这使以任何顺序生成类元素成为可能,这在某些情况下很方便。

添加或者移除类成员变量

添加和删除类成员只是在ClassNode对象的字段或方法列表中添加或删除元素。

例如,如果我们按以下方式定义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,仅当访问了所有类时才可以计算数字签名,但是添加包含它的注解为时已晚,因为必须在访问注解之前的类成员。

使用tree API时,此问题消失了,因为在这种情况下没有这种约束。

实际上,可以使用核心API来实现AddDigitialSignature示例,但随后必须通过两次转换来转换类。

在第一阶段中,使用ClassReader(而不使用ClassWriter)访问该类,以便基于该类内容计算数字签名。

在第二遍中,相同的ClassReader被重用以再次访问该类,这一次是将AddAnnotationAdapter链接到ClassWriter。

通过概括此论点,我们看到,实际上,可以单独使用核心API进行任何转换,必要时可以使用多次传递。

但是,这增加了转换代码的复杂性,这需要在遍之间存储状态(这可能像完整的树表示一样复杂!),并且多次解析类会产生一定的开销,必须将该开销与构造相应的开销进行比较。类节点。

结论是,树API通常用于无法使用核心API一次性完成的转换。

但是当然也有例外。

例如,一个混淆器不能一次实现,因为您不能在完全构造从原始名称到混淆名称的映射之前转换类,而这需要解析所有类。

但是树API也不是一个好的解决方案,因为它需要将所有类的对象表示保留在内存中以进行混淆。

在这种情况下,最好通过两次使用核心API:

一次计算原始名称和混淆名称之间的映射(一个简单的哈希表,比所有类的完整对象表示所需的内存少得多),以及一次转换基于此映射的类。

拓展阅读

更多技术文章,生活趣事,尽在【老马啸西风】。

ASM 系列详细教程-16-ASM Tree api 类接口与组件

参考文档

https://asm.ow2.io/asm4-guide.pdf

相关标签: asm