Java 类查看器
可视化地、交互式地查看 Java 类文件,了解每个字节的含义。
- 下载二进制文件 - 598 KB
- 下载源代码 - 178 KB (最后更新于 2014 年 3 月 25 日)
- 在线源代码页面 (您可以通过 NetBeans 编译最新的源代码以获得修订后的功能)
引言
Java 类文件是 Java 能够在各种平台上运行的关键原因之一。Java 类文件被设计成字节流,具有 JVM 规范第 4 章中所述的特定结构。
是的,对于初学者来说,理解 VM 规范并不容易;Java 类文件查看器是一款可视化且功能强大的应用程序,可以显示类文件中每个字节的含义。
背景
如果您对 Java 类文件查看器的历史不感兴趣,可以忽略此“背景”部分。
1. Java 类文件查看器的原因
多年前,我曾被要求编写一个可以挂钩 Java 应用程序的插件。该插件的原理很简单:在运行时找到对象/类,并尝试更改其行为。当时,我必须通过 Notepad++ 等二进制文件读取器逐字节读取类文件。这真的很有趣;很无聊。
所以,我决定编写一个 Java 类文件查看器,它可以可视化地显示类文件,并显示类文件中每个字节的含义。这就是创建 Java 类文件查看器应用程序的原因。
起初(2007 年 9 月),Java 类文件查看器是一个命令行工具。一年半后(我足够懒了……),该应用程序的图形版本被创建了。
2. Java 类文件格式库
在创建图形应用程序时,我注意到该应用程序可以分为两部分:
- Java 类文件库。它解析类字节数组文件,提供各种帮助类,这些类有助于获取已解析类的信息。如果类文件有任何问题,它可能提供比官方 JDK
javac
错误消息更友好的开发者信息。例如:使用此库,您可以轻松编写一个程序来列出 .class 文件中的所有字段和方法。 - 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_info
和CONSTANT_Double_info
会占用两个索引位置,而其他所有类型只占用一个位置。 - 超类:只有当前类为
java.lang.Object
时,超类索引才为零。否则,它应该是常量池中的一项。 - 接口和字段:一个类可能没有接口或字段,因此我们需要在获取接口/字段数组之前检查
InterfaceCount
和FieldCount
变量。 - 方法:对于非内部类,一个类必须至少有一个方法,即
javac
创建的默认实例构造函数;但对于内部类,可能没有方法。因此,我们应该检查MethodCount
变量不为零。 - 属性:一个类必须至少有一个属性,即
SourceFile
属性;我们不必为此添加类似的逻辑。
使用类似上述代码的某些代码,编写类文件查看器的 UI 控件并非难事。并且我们可以轻松编写任何应用程序来分析类中的元数据。
将已解析的类文件添加到 Swing 控件
1. 用于类文件组件层次结构的树形控件
类 org.freeinternals.javaclassviewer.ui.JTreeClassFile
是 JTree
的子类,它在构造函数中接受 ClassFile
对象。它会将类文件的所有组件添加到树形控件中。
2. 用于类文件交互式二进制查看器的分割面板
类 org.freeinternals.javaclassviewer.ui.JSplitPaneClassFile
是 JSplitPane
的子类,它被分成两个面板:左侧面板是 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_index
、descriptor_index
节点,则只有索引部分(值为 119、24)将被高亮显示。
如果我们选择 Code
属性中的 code
节点,并打开 Opcode 选项卡,则提取该方法的 opcodes。
Java 类文件查看器**不是**反编译器,它会根据上下文显示原始代码和一些注释。您可以参考 JVM 规范来了解 opcodes 的含义。提取的 opcode 部分可读。
3. 用于 ZipFile (jar, war 等) 的树形控件
类 org.freeinternals.javaclassviewer.ui.JTreeZipFile
是 JTree
的子类,它在构造函数中接受 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 日:添加了新的类报告功能;更新了下载文件