65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 类查看器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (41投票s)

2009年4月28日

MIT

8分钟阅读

viewsIcon

188875

downloadIcon

7868

可视化地、交互式地查看 Java 类文件,了解每个字节的含义。

引言

Java 类文件是 Java 能够在各种平台上运行的关键原因之一。Java 类文件被设计成字节流,具有 JVM 规范第 4 章中所述的特定结构。

是的,对于初学者来说,理解 VM 规范并不容易;Java 类文件查看器是一款可视化且功能强大的应用程序,可以显示类文件中每个字节的含义。

Java Class Viewer - Main Window

背景

如果您对 Java 类文件查看器的历史不感兴趣,可以忽略此“背景”部分。

1. Java 类文件查看器的原因

多年前,我曾被要求编写一个可以挂钩 Java 应用程序的插件。该插件的原理很简单:在运行时找到对象/类,并尝试更改其行为。当时,我必须通过 Notepad++ 等二进制文件读取器逐字节读取类文件。这真的很有趣;很无聊。

所以,我决定编写一个 Java 类文件查看器,它可以可视化地显示类文件,并显示类文件中每个字节的含义。这就是创建 Java 类文件查看器应用程序的原因。

起初(2007 年 9 月),Java 类文件查看器是一个命令行工具。一年半后(我足够懒了……),该应用程序的图形版本被创建了。

2. Java 类文件格式库

在创建图形应用程序时,我注意到该应用程序可以分为两部分:

  1. Java 类文件库。它解析类字节数组文件,提供各种帮助类,这些类有助于获取已解析类的信息。如果类文件有任何问题,它可能提供比官方 JDK javac 错误消息更友好的开发者信息。例如:使用此库,您可以轻松编写一个程序来列出 .class 文件中的所有字段和方法。
  2. Java 类文件查看器。它是一个 Swing 窗口应用程序,使用 Java 类格式库,以图形方式查看类文件。

相关库

有一些其他的库可用于类文件的创建和验证,例如 Apache 的 字节码工程库 (BCEL)。Java 类文件库的设计原理与 BCEL 有很大不同。BCEL 是一个用于编辑/更改 Java 类文件的强大库,BCEL 中还有一个类文件验证器。

嗯,使用 BCEL 编写一个可以显示每个字节含义的类文件查看器(几乎)是不可能的,因为它在解析类文件时不会记录位置;BCEL 源代码类和方法命名约定并不严格遵循 JVM 规范的类文件结构定义,因此初学者不容易理解。

嗯,Java 类文件库在解析时会记录类文件的偏移量;该库严格遵循 JVM 规范中的 ClassFile 结构。

如果您在运行 Java 类文件查看器时,同时阅读 JVM 规范,那么初学者将更容易理解类文件。

类文件格式

类文件具有以下结构:

ClassFile {
        u4 magic;
        u2 minor_version;
        u2 major_version;
        u2 constant_pool_count;
        cp_info constant_pool[constant_pool_count-1];
        u2 access_flags;
        u2 this_class;
        u2 super_class;
        u2 interfaces_count;
        u2 interfaces[interfaces_count];
        u2 fields_count;
        field_info fields[fields_count];
        u2 methods_count;
        method_info methods[methods_count];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
} 

您可能希望查看 JVM 规范中 类文件格式 部分,以获取 ClassFile 结构的详细描述。以下是简要描述:

  • magic - 0xCAFEBABE,类文件的魔数。如果前 4 个字节不是 0xCAFEBABE,则它不被识别为类文件。
  • minor_version, major_version - 主版本号和次版本号共同决定类文件版本。
  • constant_pool_count, cp_info constant_pool[constant_pool_count-1] - 类文件的常量池。常量池可能包含十一种类型的常量:
01. class/interface info
02. field reference info
03. method reference info
04. interface method reference info
05. String 
06. Integer
07. Float
08. Long
09. Double
10. NameAndType
11. Utf8 
  • access_flags - 类的访问标志。
  • this_class, super_class - 当前类和超类的类信息。只有 java.lang.Object 类的超类是 null;如果此类的超类未指定,则超类是 java.lang.Object
  • interfaces_count, interfaces[interfaces_count] - **直接**的超接口。
  • fields_count, field_info fields[fields_count] - 此类的字段(如果有)。
  • methods_count, method_info methods[methods_count] - 此类的方法。Java 编译器会为类(不包括内部类)生成一个默认构造函数,如果没有的话。因此,此类中至少有一个方法。
  • attributes_count, attribute_info attributes[attributes_count] - 此类的属性。至少有一个名为“SourceFile”的属性用于源文件名。

使用 Java 类文件库解析类文件

1. 解析类文件

org.freeinternals.format.classfile.ClassFile 是类文件的解析器。它接受字节数组作为输入参数;字节数组包含类文件。字节数组可能来自 .class 文件、.jar 文件、.war 文件等。或者,字节数组可能由 BCEL 等库构建。

// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.extractClassFile()
File file = new File("C:/Temp/File.class");
byte[] classByteArray = Tool.readClassFile(file);
ClassFile classfile = new ClassFile(classByteArray);
// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.extractJarFile()
File file = new File("C:/Temp/tools.jar");
JarFile jarFile = new JarFile(file, false, JarFile.OPEN_READ);
ZipFile zipFile = jarFile;
 final Enumeration zipEntries = zipFile.entries();
while (zipEntries.hasMoreElements()) {
    ZipEntry zipEntry = (ZipEntry) zipEntries.nextElement();
    if (!zipEntry.getName().endsWith(".class")) {
        continue;
    }
     byte[] classByteArray = Tool.readClassFile(zipFile, zipEntry);
    ClassFile classfile = new ClassFile(classByteArray);
     System.out.println();
    System.out.println(zipEntry.getName());
    jCFL_CodeDemo.printClassFile(classfile);
}

有一个工具类 org.freeinternals.javaclassviewer.ui.Tool 可以帮助我们从文件或 zip 文件读取。我们知道 .jar.war 文件实际上都是 zip 格式。

如果字节数组不是有效的类文件,ClassFile 的构造函数将抛出 org.freeinternals.format.classfile.ClassFormatException;如果有任何 IO 错误,将抛出 java.io.IOException。您可以将语句括在 try...catch 块中,或在方法声明中添加 throws 子句。

2. 获取类文件信息

成功获取 ClassFile 实例后,可以通过 getXxxxx 方法获取类文件的信息。

以下是一个打印类文件所有组件信息的示例:

// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.printClassFile()
 // Minor & Major version
MinorVersion minorVersion = classfile.getMinorVersion();
System.out.println("Class File Minor Version: " + minorVersion.getValue());
 MajorVersion majorVersion = classfile.getMajorVersion();
System.out.println("Class File Major Version: " + majorVersion.getValue());
 // Constant Pool
CPCount cpCount = classfile.getCPCount();
System.out.println("Constant Pool size: " + cpCount.getValue());
 AbstractCPInfo[] cpArray = classfile.getConstantPool();
for (int i = 1; i < cpCount.getValue(); i++) {
    System.out.println(
            String.format("Constant Pool [%d]: %s", i, classfile.getCPDescription(i)));
    short tag = cpArray[i].getTag();
    if ((tag == AbstractCPInfo.CONSTANT_Double) || 
            (tag == AbstractCPInfo.CONSTANT_Long)) {
        i++;
    }
}
 // Access flag, this & super class
AccessFlags accessFlags = classfile.getAccessFlags();
System.out.println("Class Modifier: " + accessFlags.getModifiers());
 ThisClass thisClass = classfile.getThisClass();
System.out.println("This Class Name Index: " + thisClass.getValue());
System.out.println("This Class Name: " + 
    classfile.getCPDescription(thisClass.getValue()));
 SuperClass superClass = classfile.getSuperClass();
System.out.println("Super Class Name Index: " + superClass.getValue());
if (superClass.getValue() == 0) {
    System.out.println("Super Class Name: java.lang.Object");
} else {
    System.out.println("Super Class Name: " + 
        classfile.getCPDescription(superClass.getValue()));
}
 // Interfaces
InterfaceCount interfactCount = classfile.getInterfacesCount();
System.out.println("Interface Count: " + interfactCount.getValue());
 if (interfactCount.getValue() > 0) {
    Interface[] interfaceArray = classfile.getInterfaces();
    for (int i = 0; i < interfaceArray.length; i++) {
        System.out.println(
                String.format("Interface [%d] Name Index: %d", i, 
                interfaceArray[i].getValue()));
        System.out.println(
                String.format("Interface [%d] Name: %s", i, 
        classfile.getCPDescription(interfaceArray[i].getValue())));
    }
}
 // Fields
FieldCount fieldCount = classfile.getFieldCount();
System.out.println("Field count: " + fieldCount.getValue());
 if (fieldCount.getValue() > 0) {
    FieldInfo[] fieldArray = classfile.getFields();
    for (int i = 0; i < fieldArray.length; i++) {
        System.out.println(String.format("Field [%d]: %s", i, 
                fieldArray[i].getDeclaration()));
    }
}
 // Methods
MethodCount methodCount = classfile.getMethodCount();
System.out.println("Method count: " + methodCount.getValue());
 if (methodCount.getValue() > 0) {
    MethodInfo[] methodArray = classfile.getMethods();
    for (int i = 0; i < methodArray.length; i++) {
        System.out.println(String.format("Method [%d]: %s", i, 
                methodArray[i].getDeclaration()));
    }
}
 // Attributes
AttributeCount attributeCount = classfile.getAttributeCount();
System.out.println("Attribute count: " + attributeCount.getValue());
 AttributeInfo[] attributeArray = classfile.getAttributes();
for (int i = 0; i < attributeArray.length; i++) {
    System.out.println(String.format("Attribute [%d]: %s", i, 
                attributeArray[i].getName()));
}

以下是有关上述代码的一些特别说明:

  • 常量池:常量池数组的索引从 1 到 (constant_pool_count-1);CONSTANT_Long_infoCONSTANT_Double_info 会占用两个索引位置,而其他所有类型只占用一个位置。
  • 超类:只有当前类为 java.lang.Object 时,超类索引才为零。否则,它应该是常量池中的一项。
  • 接口和字段:一个类可能没有接口或字段,因此我们需要在获取接口/字段数组之前检查 InterfaceCountFieldCount 变量。
  • 方法:对于非内部类,一个类必须至少有一个方法,即 javac 创建的默认实例构造函数;但对于内部类,可能没有方法。因此,我们应该检查 MethodCount 变量不为零。
  • 属性:一个类必须至少有一个属性,即 SourceFile 属性;我们不必为此添加类似的逻辑。

使用类似上述代码的某些代码,编写类文件查看器的 UI 控件并非难事。并且我们可以轻松编写任何应用程序来分析类中的元数据。

将已解析的类文件添加到 Swing 控件

1. 用于类文件组件层次结构的树形控件

org.freeinternals.javaclassviewer.ui.JTreeClassFileJTree 的子类,它在构造函数中接受 ClassFile 对象。它会将类文件的所有组件添加到树形控件中。

2. 用于类文件交互式二进制查看器的分割面板

org.freeinternals.javaclassviewer.ui.JSplitPaneClassFileJSplitPane 的子类,它被分成两个面板:左侧面板是 JTreeClassFile,右侧面板是类文件的二进制查看器。

当我们选择树中的每个组件时,相应的字节将被高亮显示;这就是“交互式”一词的原因。

// JavaClassViewer.src.zip - org.freeinternals.javaclassviewer.Main.open_ClassFile()
private JSplitPaneClassFile cfPane;

private void open_ClassFile(final File file) {
    this.cfPane = new JSplitPaneClassFile(Tool.readClassFile(file));
    this.add(this.cfPane, BorderLayout.CENTER);
    this.resizeForContent();
}

例如,打开类文件 File.class (java.io.File) 后,当我们选择方法 getName() 的节点时,相应的字节将被高亮显示。

如果我们选择该方法的 name_indexdescriptor_index 节点,则只有索引部分(值为 119、24)将被高亮显示。

如果我们选择 Code 属性中的 code 节点,并打开 Opcode 选项卡,则提取该方法的 opcodes。

Java 类文件查看器**不是**反编译器,它会根据上下文显示原始代码和一些注释。您可以参考 JVM 规范来了解 opcodes 的含义。提取的 opcode 部分可读。

3. 用于 ZipFile (jar, war 等) 的树形控件

org.freeinternals.javaclassviewer.ui.JTreeZipFileJTree 的子类,它在构造函数中接受 ZipFile 。它将为 zip 文件中的所有条目构建一棵树。.jar/.war 文件实际上是 zip 文件。

// JavaClassViewer.src.zip - org.freeinternals.javaclassviewer.Main.open_JarFile()
// Only key logic is left here

private JTreeZipFile zftree;

private void open_JarFile(final File file) {
    this.zftree = new JTreeZipFile(new JarFile(file, false, JarFile.OPEN_READ));
    this.zftreeContainer = new JPanelForTree(this.zftree);
    this.add(this.zftreeContainer, BorderLayout.CENTER);
    this.resizeForContent();
}

这是 ZipFile 树形控件的屏幕截图。

顺便说一句,如果我们**双击**一个 xxxxx.class 节点,将打开一个新的窗口来显示该类文件。

构建 Java 类文件查看器

基于上述可用控件,编写一个类文件查看器非常容易。我们只需要添加菜单/工具栏,并将控件布局到 JFrame 上。

您可能需要参考本文开头的源代码以获取详细信息。

类文件查看器是理解 Java 的一个很好的起点。我们可以将其作为工具来研究 JVM .class 文件级别上新 Java 版本提供的新特性。

历史

  • 2009 年 4 月 26 日:创建文章
  • 2009 年 4 月 27-29 日:更新文章
  • 2010 年 8 月 17 日:修订了本文中的链接
  • 2010 年 12 月 8 日:修订了本文中的链接
  • 2013 年 6 月 18 日:重构源代码,更新了链接,支持 Apple Mac 和 Windows 7
  • 2014 年 3 月 25 日:添加了新的类报告功能;更新了下载文件
© . All rights reserved.