[Java] 借助GUI展示class文件的版本号

58 阅读3分钟

背景

The Java® Virtual Machine Specification 有对主版本号(major version)、次版本号(minor version)的描述(在 4.1. The ClassFile Structure 这一小节)

image.png

我想到可以借助 trae 来生成一个 Python\text{Python} 脚本,这个 Python\text{Python} 提供图形化界面,可以让用户选择 class\text{class} 文件,然后展示这个 class\text{class} 文件的主版本号和次版本号。

要点

1. class 文件格式

class\text{class} 文件的格式如下表所示 ⬇️ (原始内容来自 4.1. The ClassFile Structure)

类型字段
u4magic
u2minor_version (次版本号)
u2major_version (主版本号)
u2constant_pool_count
cp_infoconstant_pool[constant_pool_count-1]
u2access_flags
u2this_class
u2super_class
u2interfaces_count
u2interfaces[interfaces_count]
u2fields_count
field_infofields[fields_count]
u2methods_count
method_infomethods[methods_count]
u2attributes_count
attribute_infoattributes[attributes_count]

本文只关心主版本号和次版本号,上表中将 主版本号 和 次版本号 用粗体展示了。

2. 如何借助 javap 命令查看主版本号和次版本号

如果我们想查看 Main.class\text{Main.class} 的 主版本号 和 次版本号,可以执行下方的命令 ⬇️

javap -v Main.class

正文

生成代码的过程

需求很明确,生成 Python\text{Python} 代码的过程如下 ⬇️

image.png

trae 最初生成的 Python\text{Python} 代码中未能支持 Java 26\text{Java 26} 以及 Java 27\text{Java 27}。4.1. The ClassFile Structure 中提到了 一个表格

image.png

于是,我又让 trae 借助 这个表格 调整代码 ⬇️

image.png

最终生成的 Python\text{Python} 代码如下

import struct
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

# 主版本号到 Java 版本的映射
JAVA_VERSION_MAP = {
    45: "Java 1.0/1.1",
    46: "Java 1.2",
    47: "Java 1.3",
    48: "Java 1.4",
    49: "Java 5",
    50: "Java 6",
    51: "Java 7",
    52: "Java 8",
    53: "Java 9",
    54: "Java 10",
    55: "Java 11",
    56: "Java 12",
    57: "Java 13",
    58: "Java 14",
    59: "Java 15",
    60: "Java 16",
    61: "Java 17",
    62: "Java 18",
    63: "Java 19",
    64: "Java 20",
    65: "Java 21",
    66: "Java 22",
    67: "Java 23",
    68: "Java 24",
    69: "Java 25",
    70: "Java 26",
    71: "Java 27",
}

# class 文件魔数: 0xCAFEBABE
MAGIC_NUMBER = 0xCAFEBABE


def parse_class_version(file_path):
    """读取 class 文件,返回 (次版本号, 主版本号)。"""
    with open(file_path, "rb") as f:
        header = f.read(8)

    if len(header) < 8:
        raise ValueError("文件太小,不是有效的 class 文件")

    magic, minor, major = struct.unpack(">IHH", header)

    if magic != MAGIC_NUMBER:
        raise ValueError(f"魔数不匹配,期望 0xCAFEBABE,实际为 0x{magic:08X}")

    return minor, major


class ClassVersionViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("Class 文件版本查看器")
        self.root.geometry("520x260")
        self.root.resizable(False, False)

        self.file_path = tk.StringVar(value="尚未选择文件")
        self.minor_version = tk.StringVar(value="-")
        self.major_version = tk.StringVar(value="-")
        self.java_version = tk.StringVar(value="-")

        self._build_ui()

    def _build_ui(self):
        pad = {"padx": 12, "pady": 8}

        # 文件选择行
        file_frame = ttk.Frame(self.root)
        file_frame.pack(fill="x", **pad)

        ttk.Button(file_frame, text="选择 class 文件", command=self._choose_file).pack(side="left")
        ttk.Label(file_frame, textvariable=self.file_path, foreground="gray", wraplength=380).pack(side="left", padx=(8, 0))

        # 结果展示区
        result_frame = ttk.LabelFrame(self.root, text="版本信息")
        result_frame.pack(fill="both", expand=True, **pad)

        rows = [
            ("次版本号 (minor version):", self.minor_version),
            ("主版本号 (major version):", self.major_version),
            ("对应 Java 版本:", self.java_version),
        ]
        for i, (label_text, value_var) in enumerate(rows):
            ttk.Label(result_frame, text=label_text).grid(row=i, column=0, sticky="w", padx=8, pady=6)
            ttk.Label(result_frame, textvariable=value_var, font=("Menlo", 12, "bold")).grid(row=i, column=1, sticky="w", padx=8, pady=6)

        result_frame.columnconfigure(0, weight=1)
        result_frame.columnconfigure(1, weight=2)

    def _choose_file(self):
        path = filedialog.askopenfilename(
            title="选择 class 文件",
            filetypes=[("Java class 文件", "*.class"), ("所有文件", "*.*")],
        )
        if not path:
            return

        self.file_path.set(path)
        try:
            minor, major = parse_class_version(path)
        except (ValueError, OSError) as e:
            messagebox.showerror("错误", str(e))
            self._reset_results()
            return

        self.minor_version.set(str(minor))
        self.major_version.set(str(major))
        self.java_version.set(JAVA_VERSION_MAP.get(major, "未知 (可能是较新的版本)"))

    def _reset_results(self):
        for var in (self.minor_version, self.major_version, self.java_version):
            var.set("-")


def main():
    root = tk.Tk()
    ClassVersionViewer(root)
    root.mainloop()


if __name__ == "__main__":
    main()

运行效果

运行效果如下 ⬇️

image.png

选择了一个 class\text{class} 文件后,处理结果如下 ⬇️

image.png

补充说明

本工具只读取 class 文件最开始 8 字节,不解析完整的 class 文件

借助 javap 命令来查看主版本号和次版本号

可以借助 javap\text{javap} 命令来查看 class\text{class} 文件的主版本号和次版本号。示例如下(假设我们已经有一个名为 Main.class\text{Main.class} 的文件了)

javap -v Main.class

运行结果如下图所示 ⬇️

image.png

运行环境提示

  1. 运行脚本需要本机有 Python
  2. 如果您没有安装 Tkinter 的话,也需要安装一下才能运行本文提供的 Python 脚本

参考资料