泛型与 ASM

830 阅读20分钟
原文链接: www.ibm.com

Classworking 工具箱

泛型与 ASM

了解如何使用 ASM 字节码框架从 Java 5 中访问泛型信息

系列内容:

此内容是该系列的一部分:Classworking 工具箱

Java 5 程序中的泛型信息对于理解程序的数据结构非常有帮助。在 上一期 中,我为您介绍了如何使用运行时反射来访问泛型信息。如果您仅对获得载入 JVM 中的类的信息感兴趣,那么这种反射方法非常有效。但有时您可能希望在载入类之前对其加以修改,或者希望在不载入类的情况下研究数据结构。在这样的时候,反射对您来说就不再是一种行之有效的办法 —— 反射将 JVM 类结构作为信息源使用,因此它仅对已由 JVM 装载的类起作用。

要想在不将类载入 JVM 的情况下访问泛型信息,您需要一种读取存储在二进制类表示内的泛型信息的方法。在前几期文章中,已经介绍过 ASM classworking 库是怎样提供了一种清洁的接口,以读取及写入二进制类。在这篇文章中,我将向您展示如何利用 ASM 从类文件中获取原始泛型信息,如何以一种有用的方式解释泛型。在钻研 ASP 细节之前,让我们首先来看看泛型信息编码到二进制类中的实际方式。

跟踪泛型

为将可由 Java 编译器使用的键入信息添加到 Java 二进制类中,需要使用泛型规范设计器。幸运的是,Java 平台已有一种内置于二进制类格式中的机制,可用于此目的。这种机制就是属性 结构 (attribute structure),它主要使所有类型的信息可与类本身或类的方法、字段及其他组件相关联。某些类型的属性信息是由 JVM 规范定义的,但 Java 语言的原始设计器作出了明智的选择,将一组可能出现的属性保留为开放,从而可由新版本的规范加以扩展,也可由用户扩展以设计其自己的自定义属性。

泛型信息存储在一个新的标准属性中:签名 属性。该属性是一个简单的文本值,为类、字段、方法或变量解码泛型信息。更新的 Java 5 JVM 规范(参考资料 部分给出了 Java 5 更改页面的链接)清楚地说明了签名文本值的完整语法。在这里我不打算加以详述,但本节稍后的部分中会简单介绍签名。首先将介绍一些必备的背景信息,以使您了解类名称的内部结构及 JVM 所使用的字段和方法描述符。

深入内部

Java 平台中的类总是来自某些包。当您在 Java 源代码中引用类名称时,您或许会也或许不会真正将包限定作为名称的一部分。您总是可以 包含包限定(形如 java.lang.String),但您也可以为了省事而忽略它 —— 如果类来自 java.lang 包或已 import 到源文件中。这种包含包限定的类名称结构就称为 “完全限定” 类名。

在实际的二进制类内部,类名称总是在一个包中指定的。但这种名称的格式与 Java 源代码中的完全限定类名略有差别,使用正斜杠 (“/”) 取代圆点 (“.”)。例如,在 String 类中,名称的内部形式java/lang/String。如果您尝试将一个类文件作为文本输出或查看,那么通常会看到上述形式的多个字符串,每个字符串都是对某个类的引用。

采用这种内部形式的类引用是作为字段和方法描述符的一部分使用的。字段描述符 指定类中定义的一个类的准确类型。所使用的表示法取决于字段是简单对象类型、简单原语类型还是数组类型。简单对象类型的表示法为,以 ‘L’ 开头,后接对象类名称的内部形式,以 ‘;’ 结尾。原语类型的表示法为,各类型使用一个单独的字母(如 ‘I’ 表示 int、‘Z’ 表示布尔型)。数组类型的表示法为,以 ‘[’ 作为数组项类型(其本身也可为数组类型)的前缀修饰符。表 1 给出了关于各字段描述符的示例,另外还列出了相应的 Java 源代码声明:

表 1. 字段描述符示例
描述符 源代码
Ljava/lang/String; String
I int
[Ljava/lang/Object; Object[]
[Z boolean[]
[[Lcom/sosnoski/generics/FileInfo; com.sosnoski.generics.FileInfo[][]

方法描述符 结合了字段描述符,以指定方法的参数类型和返回类型。方法描述符的格式非常易于理解。以 ‘(’ 开始,后接参数的字段描述符(均一起运行),随后是 ‘)’,最后以返回类型结尾(若返回类型为 void,则以 ‘V’ 结尾)。表 2 给出了方法描述符的一些示例,同时还列出了相应的 Java 源代码声明(注意方法名称和参数名称本身并非方法描述符的一部分,所以在表中使用了占位符):

表 2. 方法描述符示例
描述符 源代码
(Ljava/lang/String;)I int mmm(String x)
(ILjava/lang/String;)V void mmm(int x, String y)
(I)Ljava/lang/String; String mmm(int x)
(Ljava/lang/String;)[C char[] mmm(String x)
(ILjava/lang/String;[[Lcom/sosnoski/generics/FileInfo;)V void mmm(int x, String y, FileInfo[][] z)

在虚线处签名

上面已经介绍了字段和方法描述符,那么接下来将介绍签名。签名格式扩展了字段和方法描述符的概念,将泛型类型信息包含于其中。不幸的是,泛型的复杂性(包括可能出现的各种上下界变化等)意味着签名无法像描述符那样简单地说明。签名的语法(详见 JVM specification changes for Java 1.5 的第 4 章)包含 21 个独立产品项。本文无法全面涉及,这里将给出几个示例,下一节将针对这部分示例展开讲解。

清单 1 是 上一期 文章中所用的一个数据结构类的部分源代码,以及相应的签名字符串。在本例中,类本身并非参数化类型,但字段和方法使用了参数化的 java.util.List

清单 1. 简单的签名示例
public class DirInfo
{
    private final List<FileInfo> m_files;
    private final List<DirInfo> m_directories;
    ...    
    public List<DirInfo> getDirectories() {
        return m_directories;
    }
    public List<FileInfo> getFiles() {
        return m_files;
    }
    ...
}
 
Class signature:
 {none}
m_files signature:
 Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
m_directories signature:
 Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
getDirectories() signature:
 ()Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
getFiles() signature:
 ()Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;

由于类并非参数化类型,所以未为该类本身的二进制类表示添加任何签名。但确实 为使用参数化类型的字段和方法使用了签名。 m_files 字段签名表示这是一个 List,且类型为 FileInfo;而 m_directories 字段签名则表示这是一个类型为 DirInfoList。同样, getDirectories() 方法签名表示该方法返回一个类型为 DirInfoList,而 getFiles() 签名则表示该方法返回一个类型为 FileInfoList

迄今为止,一切看起来都相当容易理解,但事实真是如此吗?下面让我们看看清单 2,其中给出了一个简单的参数化类定义和相应的签名字符串:

清单 2. 参数化类签名示例
public class PairCollection<T,U> implements Iterable<T>
{
    /** Collection with first component values. */
    private final ArrayList<T> m_tValues;
     
    /** Collection with second component values. */
    private final ArrayList<U> m_uValues;
    ...
    public void add(T t, U u) {
        m_tValues.add(t);
        m_uValues.add(u);
    }
     
    public U get(T t) {
        int index = m_tValues.indexOf(t);
        if (index >= 0) {
            return m_uValues.get(index);
        } else {
            return null;
        }
    }
    ...
}
 
Class signature:
 <T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/Iterable<TT;>;
m_tValues signature:
 Ljava/util/ArrayList<TT;>;
m_uValues signature:
 Ljava/util/ArrayList<TU;>;
add signature:
 (TT;TU;)V
get signature:
 (TT;)TU;

由于清单 2 中的类为参数化类型,所以类签名需要以二进制类形式表示。与源代码相比,签名的文本要长一些,但如果您了解到,源代码中省略的类型参数的所有可选组件都包含在签名中,那么理解起来也就不太困难了。签名的第一部分(位于尖括号 ‘<...>’ 内)就是该类的类型参数定义清单。这些定义的形式都相同,类型参数名称后接类型的类边界和接口边界(若存在)的字段描述符。各字段描述符前加 ‘:’ 字符。由于清单 2 源代码未为类的类型参数指定任何边界,因此其边界均为默认的类边界 java.lang.Object

类签名的第二部分(尖括号外)给出了超类和超接口(若存在)的签名。在清单 2 所示的例子中,未指定任何超类,因此签名以 java.lang.Object 作为超类。这里指定了超接口,Iterable<T>。在签名中可以看到预期结果,源代码中使用的只是 ‘<T>’,而签名中使用的是 ‘<TT;>’。原因在于签名需要区分类名称和类型变量名称,第一个 ‘T’标识紧随其后的内容为类型变量名,而结尾的‘;’ 表示名称结束。

清单 2 中的字段和方法签名利用了与超接口签名相同的变量格式类型,其他都与前面介绍的内容相同。

ASM 中的泛型

本系列的前几期文章中已介绍过(链接参见 参考资料 部分),ASM 使用了一种访问器 (visitor) 模式来处理二进制类表示。这种访问器模式是双向的:您可以解析一个现有类,得到类组件的处理程序访问器方法的调用序列,也可以实现对类写入器的访问器方法的同类调用序列,以生成一个二进制类表示。这一解析器/写入器对称使 ASM 在您仅修改类的特定方面的情况下尤为方便 —— 您可将类写入器作为类解析器事件的处理程序的基础,仅重写基写入器来处理您想更改的事件。解析器(或读取器)和写入器都是非常有用的独立组件。

ASM 2.X 全面支持 Java 5 JVM 更改,包括读取和写入签名。签名的基本处理是通过直接传递给恰当的访问器方法的值自动实现的。另外,ASM 2.X 还增加了对签名字符串(有时非常复杂)编码进行解析的支持,从而可翻译签名细节。按照 ASM 的基本原理,相同的接口还可供写入器使用以按需生成签名字符串。在这一节中,我将介绍 ASM 如何将基本签名作为 text blob 处理,又是如何详细解析基本签名的。

所有部分的签名

ASM 中将签名作为 text blob 处理,这一方式直接内建于基本类、字段和方法的访问器调用中。清单 3 展示了 org.objectweb.asm.ClassVisitor 接口中的相应方法:

清单 3. 类、字段和方法的访问器方法
public interface ClassVisitor
{
    void visit(int version, int access, String name, String signature,
        String superName, String[] interfaces);
         
    FieldVisitor visitField(int access, String name, String desc,
        String signature, Object value);
         
    MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions);
    ...
}

清单中的各访问器方法将签名字符串作为参数。若相应的类、字段或方法非泛型,则在调用方法时将返回 null 值。

清单 4 显示了签名相关方法的实际应用。其中用 org.objectweb.asm.commons.EmptyVisitor 类作为基础实现了一个访问器类,这样我只需重写想使用的方法即可。所提供的方法实现仅输出整体签名信息、本清单所示类中各字段和方法的描述符和签名信息。清单 4 的末尾处展示了在 清单 1 所示的完整 DirInfo 类中使用此访问器时所生成的输出:

清单 4. 签名相关方法的实际应用
public class ShowSignaturesVisitor extends EmptyVisitor
{
    public void visit(int version, int access, String name, String sig,
        String sname, String[] inames) {
        System.out.println("Class " + name + " signature:");
        System.out.println(" " + sig);
        super.visit(version, access, name, sig, sname, inames);
    }
 
    public FieldVisitor visitField(int access, String name, String desc,
        String sig, Object value) {
        System.out.println("Field " + name + " descriptor and signature:");
        System.out.println(" " + desc);
        System.out.println(" " + sig);
        return super.visitField(access, name, desc, sig, value);
    }
 
    public MethodVisitor visitMethod(int access, String name, String desc,
        String sig, String[] exceptions) {
        System.out.println("Method " + name + "() descriptor and signature:");
        System.out.println(" " + desc);
        System.out.println(" " + sig);
        return super.visitMethod(access, name, desc, sig, exceptions);
    }
}
 
Class com/sosnoski/generics/DirInfo signature:
 null
Field m_files descriptor and signature:
 Ljava/util/List;
 Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
Field m_directories descriptor and signature:
 Ljava/util/List;
 Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
Field m_lastModify descriptor and signature:
 Ljava/util/Date;
 null
Method <init>() descriptor and signature:
 (Ljava/io/File;)V
 null
Method getDirectories() descriptor and signature:
 ()Ljava/util/List;
 ()Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
Method getFiles() descriptor and signature:
 ()Ljava/util/List;
 ()Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
Method getLastModify() descriptor and signature:
 ()Ljava/util/Date;
 null

签名分析

除将签名作为字符串处理外,ASM 还支持在细节级处理签名。org.objectweb.asm.signature.SignatureReader 类解析一个签名字符串,并生成对 org.objectweb.asm.signature.SignatureVisitor 接口的调用序列。org.objectweb.asm.signature.SignatureWriter 类实现访问器接口,并从访问器方法调用序列中构建出签名字符串。

很不幸,细节级接口有些复杂,但其原因在于签名定义的复杂性,而不是 ASM 代码处理不力。SignatureVisitor 接口展现了这一复杂性,它定义了 16 个可在签名处理过程中包含的独立方法调用。当然,绝大多数签名仅使用这些方法中的一小部分。

为举例说明 ASM 的细节级签名处理,我将解析本文前面所讨论的某些签名,从而介绍方法。为此,我编写了 TraceSignatureVisitor 类,清单 5 展示了该类的部分代码,该清单中的 AnalyzeSignaturesVisitor 用于驱动签名处理。当 AnalyzeSignaturesVisitor 用做类的访问器时,它会为所发现的各签名创建一个 SignatureReader,将 TraceSignatureVisitor 类的一个实例作为签名组件访问器调用的目标传递。用于解析签名的 SignatureReader 调用取决于签名的形式:对于类和方法签名,恰当的方法是 accept();对于字段签名,应使用 acceptType() 调用。

清单 5. 签名分析
public class TraceSignatureVisitor implements SignatureVisitor
{
    public void visitFormalTypeParameter(String name) {
        System.out.println("  visitFormalTypeParameter(" + name + ")");
    }
 
    public SignatureVisitor visitClassBound() {
        System.out.println("  visitClassBound()");
        return this;
    }
 
    public SignatureVisitor visitInterfaceBound() {
        System.out.println("  visitInterfaceBound()");
        return this;
    }
 
    public SignatureVisitor visitSuperclass() {
        System.out.println("  visitSuperclass()");
        return this;
    }
 
    public SignatureVisitor visitInterface() {
        System.out.println("  visitInterface()");
        return this;
    }
 
    public SignatureVisitor visitParameterType() {
        System.out.println("  visitParameterType()");
        return this;
    }
    ...
}
 
public class AnalyzeSignaturesVisitor extends EmptyVisitor
{
    public void visit(int version, int access, String name, String sig,
        String sname, String[] inames) {
        if (sig != null) {
            System.out.println("Class " + name + " signature:");
            System.out.println(" " + sig);
            new SignatureReader(sig).accept(new TraceSignatureVisitor());
        }
        super.visit(version, access, name, sig, sname, inames);
    }
 
    public FieldVisitor visitField(int access, String name, String desc,
        String sig, Object value) {
        if (sig != null) {
            System.out.println("Field " + name + " signature:");
            System.out.println(" " + sig);
            new SignatureReader(sig).acceptType(new TraceSignatureVisitor());
        }
        return super.visitField(access, name, desc, sig, value);
    }
 
    public MethodVisitor visitMethod(int access, String name, String desc,
        String sig, String[] exceptions) {
        if (sig != null) {
            System.out.println("Method " + name + "() signature:");
            System.out.println(" " + sig);
            new SignatureReader(sig).accept(new TraceSignatureVisitor());
        }
        return super.visitMethod(access, name, desc, sig, exceptions);
    }
}

清单 6 显示了使用 AnalyzeSignaturesVisitor 类访问 清单 1 中的 DirInfo 类时所生成的输出:

清单 6. DirInfo 代码和签名分析
public class DirInfo
{
    private final List<FileInfo> m_files;
    private final List<DirInfo> m_directories;
    ...    
    public List<DirInfo> getDirectories() {
        return m_directories;
    }
    public List<FileInfo> getFiles() {
        return m_files;
    }
    ...
}
 
Field m_files signature:
 Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
  visitClassType(java/util/List)
  visitTypeArgument(=)
  visitClassType(com/sosnoski/generics/FileInfo)
  visitEnd()
  visitEnd()
Field m_directories signature:
 Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
  visitClassType(java/util/List)
  visitTypeArgument(=)
  visitClassType(com/sosnoski/generics/DirInfo)
  visitEnd()
  visitEnd()
Method getDirectories() signature:
 ()Ljava/util/List<Lcom/sosnoski/generics/DirInfo;>;
  visitReturnType()
  visitClassType(java/util/List)
  visitTypeArgument(=)
  visitClassType(com/sosnoski/generics/DirInfo)
  visitEnd()
  visitEnd()
Method getFiles() signature:
 ()Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>;
  visitReturnType()
  visitClassType(java/util/List)
  visitTypeArgument(=)
  visitClassType(com/sosnoski/generics/FileInfo)
  visitEnd()
  visitEnd()

清单 6 中输出行的第一块展示了 m_files 签名 Ljava/util/List<Lcom/sosnoski/generics/FileInfo;>; 的分析过程中所调用的访问器方法。第一个方法调用是 visitClassType("java/util/List"),给出了字段的基类。随后 visitTypeArgument("=") 说明实际类型由当前类的类型参数 (java.util.List) 提供,visitClassType("com/sosnoski/generics/FileInfo") 说明实际类型以 com.sosnoski.generics.FileInfo 为基础。最终,对 visitEnd() 的第一个调用关闭了打开的 FileInfo 类签名,第二个调用关闭了打开的 List 类签名。

通过观察访问器方法调用序列,您或许已经猜到,其中部分调用有效地为嵌入的类型签名组件打开了一个新的上下文。SignatureVisitor 接口中返回 SignatureVisitor 实例的方法均有此作用。方法调用所返回的接口实例(可能与被调用的实例相同,也可能不同,清单 5 代码中就是相同的)随后用于处理嵌入的类型签名。可很容易地对 清单 5 所示代码作出修改,以缩进格式展示子签名嵌套,本文提供的下载文件中也包含更改后的代码。这里不准备给出详细的代码,仅介绍输出结果。清单 7 给出了在 清单 2 PairCollection 参数化类上运行此缩进版代码时所生成的输出结果(部分):

清单 7. PairCollection 代码和签名分析
public class PairCollection<T,U> implements Iterable<T>
{
    /** Collection with first component values. */
    private final ArrayList<T> m_tValues;
     
    /** Collection with second component values. */
    private final ArrayList<U> m_uValues;
    ...
    public void add(T t, U u) {
        m_tValues.add(t);
        m_uValues.add(u);
    }
     
    public U get(T t) {
        int index = m_tValues.indexOf(t);
        if (index >= 0) {
            return m_uValues.get(index);
        } else {
            return null;
        }
    }
    ...
}
 
Class com/sosnoski/generics/PairCollection signature:
 <T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;Ljava/lang/Iterable<TT;>;
  visitFormalTypeParameter(T)
  visitClassBound()
   visitClassType(java/lang/Object)
   visitEnd()
  visitFormalTypeParameter(U)
  visitClassBound()
   visitClassType(java/lang/Object)
   visitEnd()
  visitSuperclass()
   visitClassType(java/lang/Object)
   visitEnd()
  visitInterface()
   visitClassType(java/lang/Iterable)
   visitTypeArgument(=)
    visitTypeVariable(T)
   visitEnd()
Field m_tValues signature:
 Ljava/util/ArrayList<TT;>;
  visitClassType(java/util/ArrayList)
  visitTypeArgument(=)
   visitTypeVariable(T)
  visitEnd()
Field m_uValues signature:
 Ljava/util/ArrayList<TU;>;
  visitClassType(java/util/ArrayList)
  visitTypeArgument(=)
   visitTypeVariable(U)
  visitEnd()
Method add() signature:
 (TT;TU;)V
  visitParameterType()
   visitTypeVariable(T)
  visitParameterType()
   visitTypeVariable(U)
  visitReturnType()
   visitBaseType(V)
Method get() signature:
 (TT;)TU;
  visitParameterType()
   visitTypeVariable(T)
  visitReturnType()
   visitTypeVariable(U)

清单 7 输出显示了嵌套类型定义在被解析的签名中的使用方法。在处理类签名时,嵌套可能深达两层 —— 类签名包含一个类必须实现的接口签名,接口签名又包含一个类型参数签名(也就是本例中的类型变量 “T”)。

进一步了解 ASM 泛型

本文介绍了一些基础知识,包括泛型信息在二进制类表示中的存储方式及使用 ASM 访问泛型信息的方法等。下个月我将引入一种围绕 ASM 构建的递归数据结构分析器,完成泛型的介绍。该分析器从初始类开始将引用的所有类贯穿起来,在此过程中处理泛型类型的置换。最终得到一种数据结构,反射了通过使用泛型可推导出的所有信息。

下载资源

相关主题

  • Generics in the Java Programming Language”(Sun Microsystems,2004 年):Gilad Bracha 所撰写的关于泛型的教程,他是 Java 语言中泛型支持的主要架构师。
  • Java 理论和实践: 了解泛型”(developerWorks,2005 年):Brian Goetz 介绍了使用泛型的实践技巧和洞察。
  • Classworking 工具箱: 注释(Annotation)与 ASM”(developerWorks,2005 年):Dennis Sosnoski 展示了使用注释来触发载入时 ASM 对代码的修改。
  • JVM Changes for JDK 1.5(Sun Microsystems,2005 年):给出了 Java 5 所需要的 JVM 规范更新的链接(大概在全文中间的位置)。关于签名属性的细节更改,请参阅第 4 章。
  • Java 编程动态:系列作者 Dennis Sosnoski 引领您进入 Java 类结构、反射和 classworking 的世界。
  • ASM:获得 ASM Java 字节码操纵框架。
  • Java 技术专区:查看关于 Java 编程各方面的参考资料。