背景
The Java® Virtual Machine Specification 中的 4.1. The ClassFile Structure 这一小节有对 access_flags 的介绍 ⬇️

我们可以借助 trae 来生成一个 Python 脚本,这个 Python 提供图形化界面,让用户选择 class 文件,然后展示这个 class 文件顶层的 access_flags。示例效果如下图所示 ⬇️

要点
1. class 文件格式
class 文件的格式如下表所示 ⬇️ (原始内容来自 4.1. The ClassFile Structure)
| 类型 | 字段 |
|---|---|
| u4 | magic |
| u2 | minor_version |
| u2 | major_version |
| u2 | constant_pool_count |
| cp_info | constant_poolconstant_pool_count-1 |
| u2 | access_flags |
| u2 | this_class |
| u2 | super_class |
| u2 | interfaces_count |
| u2 | interfacesinterfaces_count |
| u2 | fields_count |
| field_info | fieldsfields_count |
| u2 | methods_count |
| method_info | methodsmethods_count |
| u2 | attributes_count |
| attribute_info | attributesattributes_count |
本文只关心 access_flags,上表中将它用粗体展示了。
2. 如何借助 javap 命令查看 access_flags
如果我们想查看 Main.class 的 access_flags,可以执行下方的命令 ⬇️
bash
javap -v Main.class
正文
生成代码的过程
4.1. The ClassFile Structure 中介绍了 class 文件的结构,要想获取 access_flags,我们需要跳过下图红色框所示的这些内容 ⬇️

其中
magic长度固定为 4 字节minor_version长度固定为 2 字节major_version长度固定为 2 字节constant_pool_count长度固定为 2 字节constant_pool长度 不固定
所以难点在于如何跳过 constant_pool。根据 4.4. The Constant Pool 中的描述可知,constant_pool 共支持 17 种 tag ⬇️

trae 帮我汇总的常量池中各个 tag 的长度信息
(这一小节的内容由 trae 生成,我做了些调整)
来源: JVMS §4.4, Table 4.4-A (Java SE 27)
每个常量池条目以 1 字节的 tag 开头, tag 之后跟着若干字节的附加信息, 格式由 tag 决定。 下表按 tag 值升序排列, "字节数"指 tag 之后 的附加信息长度, 各结构的字段布局来自 §4.4.1--§4.4.12。
| Tag | 常量类型 | 结构 (tag 之后) | 字节数 | 位置 |
|---|---|---|---|---|
| 1 | CONSTANT_Utf8 |
u2 length; u1 bytes[length] |
变长 (2 + length) | §4.4.7 |
| 3 | CONSTANT_Integer |
u4 bytes |
4 | §4.4.4 |
| 4 | CONSTANT_Float |
u4 bytes |
4 | §4.4.4 |
| 5 | CONSTANT_Long |
u4 high_bytes; u4 low_bytes |
8 | §4.4.5 |
| 6 | CONSTANT_Double |
u4 high_bytes; u4 low_bytes |
8 | §4.4.5 |
| 7 | CONSTANT_Class |
u2 name_index |
2 | §4.4.1 |
| 8 | CONSTANT_String |
u2 string_index |
2 | §4.4.3 |
| 9 | CONSTANT_Fieldref |
u2 class_index; u2 name_and_type_index |
4 | §4.4.2 |
| 10 | CONSTANT_Methodref |
u2 class_index; u2 name_and_type_index |
4 | §4.4.2 |
| 11 | CONSTANT_InterfaceMethodref |
u2 class_index; u2 name_and_type_index |
4 | §4.4.2 |
| 12 | CONSTANT_NameAndType |
u2 name_index; u2 descriptor_index |
4 | §4.4.6 |
| 15 | CONSTANT_MethodHandle |
u1 reference_kind; u2 reference_index |
3 | §4.4.8 |
| 16 | CONSTANT_MethodType |
u2 descriptor_index |
2 | §4.4.9 |
| 17 | CONSTANT_Dynamic |
u2 bootstrap_method_attr_index; u2 name_and_type_index |
4 | §4.4.10 |
| 18 | CONSTANT_InvokeDynamic |
u2 bootstrap_method_attr_index; u2 name_and_type_index |
4 | §4.4.10 |
| 19 | CONSTANT_Module |
u2 name_index |
2 | §4.4.11 |
| 20 | CONSTANT_Package |
u2 name_index |
2 | §4.4.12 |
备注
u1= 无符号 1 字节,u2= 无符号 2 字节(大端序),u4= 无符号 4 字节(大端序)。CONSTANT_Utf8是唯一变长的条目, 总长度 = 1(tag) + 2(length) + length。CONSTANT_Long(5) 和CONSTANT_Double(6) 虽然只占 8 字节, 但在常量池中占用 2 个 slot, 即常量池索引会跳过一个位置 --- 遍历常量池时必须特殊处理。- 同一小节内的常量类型结构完全相同, 仅 tag 值不同:
- §4.4.2: Fieldref / Methodref / InterfaceMethodref
- §4.4.4: Integer / Float
- §4.4.5: Long / Double
- §4.4.10: Dynamic / InvokeDynamic
完整的代码
经过与 trae 和 豆包 的若干轮互动,最终得到了如下的 Python 代码
python
import struct
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
# class 文件魔数: 0xCAFEBABE
MAGIC_NUMBER = 0xCAFEBABE
# 常量池 tag -> (类型名称, 后续字节数)
# 注: CONSTANT_Long(5) 和 CONSTANT_Double(6) 占用 2 个 slot
CONSTANT_POOL_TAGS = {
1: ("CONSTANT_Utf8", None), # 变长: 2字节 length + length 字节
3: ("CONSTANT_Integer", 4),
4: ("CONSTANT_Float", 4),
5: ("CONSTANT_Long", 8), # 占用 2 个 slot
6: ("CONSTANT_Double", 8), # 占用 2 个 slot
7: ("CONSTANT_Class", 2),
8: ("CONSTANT_String", 2),
9: ("CONSTANT_Fieldref", 4),
10: ("CONSTANT_Methodref", 4),
11: ("CONSTANT_InterfaceMethodref", 4),
12: ("CONSTANT_NameAndType", 4),
15: ("CONSTANT_MethodHandle", 3),
16: ("CONSTANT_MethodType", 2),
17: ("CONSTANT_Dynamic", 4),
18: ("CONSTANT_InvokeDynamic", 4),
19: ("CONSTANT_Module", 2),
20: ("CONSTANT_Package", 2),
}
# access_flags 位掩码 (JVMS §4.1 Table 4.1-B)
ACCESS_FLAGS = [
("ACC_PUBLIC", 0x0001,
"Declared public; may be accessed from outside its package."),
("ACC_FINAL", 0x0010, "Declared final; no subclasses allowed."),
("ACC_SUPER", 0x0020,
"Treat superclass methods specially when invoked by the "
"invokespecial instruction."),
("ACC_INTERFACE", 0x0200, "Is an interface, not a class."),
("ACC_ABSTRACT", 0x0400, "Declared abstract; must not be instantiated."),
("ACC_SYNTHETIC", 0x1000,
"Declared synthetic; not present in the source code."),
("ACC_ANNOTATION", 0x2000, "Declared as an annotation interface."),
("ACC_ENUM", 0x4000, "Declared as an enum class."),
("ACC_MODULE", 0x8000, "Is a module, not a class or interface."),
]
def skip_constant_pool(data, offset, count):
"""跳过常量池, 返回 access_flags 的偏移量。
常量池索引从 1 到 count-1。
"""
index = 1
while index < count:
if offset >= len(data):
raise ValueError("常量池数据不完整, 文件可能被截断")
tag = data[offset]
offset += 1
if tag not in CONSTANT_POOL_TAGS:
raise ValueError(f"未知的常量池 tag: {tag} (索引 {index})")
name, size = CONSTANT_POOL_TAGS[tag]
if name == "CONSTANT_Utf8":
if offset + 2 > len(data):
raise ValueError(
"CONSTANT_Utf8 长度字段不完整, 文件可能被截断")
(length,) = struct.unpack_from(">H", data, offset)
offset += 2
if offset + length > len(data):
raise ValueError(
"CONSTANT_Utf8 内容超出文件末尾, 文件可能被截断")
offset += length
else:
if offset + size > len(data):
raise ValueError(f"{name} 数据不完整, 文件可能被截断")
offset += size
# CONSTANT_Long 和 CONSTANT_Double 占 2 个 slot
if name in ("CONSTANT_Long", "CONSTANT_Double"):
index += 2
else:
index += 1
return offset
def parse_access_flags(file_path):
"""解析 class 文件, 返回 (minor, major, constant_pool_count, access_flags)。"""
with open(file_path, "rb") as f:
data = f.read()
if len(data) < 10:
raise ValueError("文件太小, 不是有效的 class 文件")
# 格式字符串 ">IHHH" 含义:
# > --- 大端序(网络字节序), class 文件采用大端序
# I --- 无符号 4 字节整数, 读取 magic (魔数)
# H --- 无符号 2 字节整数, 读取 minor_version (次版本号)
# H --- 无符号 2 字节整数, 读取 major_version (主版本号)
# H --- 无符号 2 字节整数, 读取 constant_pool_count (常量池条目数)
magic, minor, major, cp_count = struct.unpack_from(">IHHH", data, 0)
if magic != MAGIC_NUMBER:
raise ValueError(f"魔数不匹配, 期望 0xCAFEBABE, 实际为 0x{magic:08X}")
# constant_pool_count 等于常量池条目数 + 1, 因此最小为 1
if cp_count < 1:
raise ValueError(f"constant_pool_count 非法: {cp_count} (必须 >= 1)")
offset = 10 # magic(4) + minor(2) + major(2) + constant_pool_count(2)
offset = skip_constant_pool(data, offset, cp_count)
if offset + 2 > len(data):
raise ValueError("access_flags 字段不完整, 文件可能被截断")
(access_flags,) = struct.unpack_from(">H", data, offset)
return minor, major, cp_count, access_flags
class AccessFlagsViewer:
def __init__(self, root):
self.root = root
self.root.title("Class 文件 access_flags 查看器")
self.root.resizable(False, False)
self.file_path = tk.StringVar(value="尚未选择文件")
self.minor_version = tk.StringVar(value="-")
self.major_version = tk.StringVar(value="-")
self.cp_count = tk.StringVar(value="-")
self.access_flags_hex = tk.StringVar(value="-")
self._build_ui()
# 宽度固定 880, 高度取内容自然高度(明细表恰好 9 行)
self.root.update_idletasks()
self.root.geometry(f"880x{self.root.winfo_reqheight()}")
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")
path_label = ttk.Label(file_frame, textvariable=self.file_path,
foreground="gray", wraplength=600)
path_label.pack(side="left", padx=(8, 0))
# 基础信息区
info_frame = ttk.LabelFrame(self.root, text="基础信息")
info_frame.pack(fill="x", **pad)
rows = [
("次版本号 (minor_version):", self.minor_version),
("主版本号 (major_version):", self.major_version),
("常量池条目数 (constant_pool_count):", self.cp_count),
("access_flags (十六进制):", self.access_flags_hex),
]
for i, (label_text, value_var) in enumerate(rows):
ttk.Label(info_frame, text=label_text).grid(
row=i, column=0, sticky="w", padx=8, pady=4)
value_label = ttk.Label(info_frame, textvariable=value_var,
font=("Menlo", 11, "bold"))
value_label.grid(row=i, column=1, sticky="w", padx=8, pady=4)
info_frame.columnconfigure(1, weight=1)
# flags 明细表
flags_frame = ttk.LabelFrame(self.root, text="access_flags 位明细")
flags_frame.pack(fill="x", **pad)
columns = ("flag", "value", "set", "interpretation")
self.tree = ttk.Treeview(flags_frame, columns=columns,
show="headings", height=9)
self.tree.heading("flag", text="Flag Name")
self.tree.heading("value", text="Value")
self.tree.heading("set", text="Set?")
self.tree.heading("interpretation", text="Interpretation")
self.tree.column("flag", width=140)
self.tree.column("value", width=80)
self.tree.column("set", width=60)
self.tree.column("interpretation", width=560)
# 配置高亮 tag: 用于 "Set?" 为 Yes 的行
self.tree.tag_configure("set", background="#FFF3B0",
foreground="#B8860B")
self.tree.pack(fill="x")
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, cp_count, flags = parse_access_flags(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.cp_count.set(str(cp_count))
self.access_flags_hex.set(f"0x{flags:04X}")
self._update_flags_table(flags)
def _update_flags_table(self, flags):
self.tree.delete(*self.tree.get_children())
for name, value, desc in ACCESS_FLAGS:
is_set = "Yes" if flags & value else "No"
tags = ("set",) if flags & value else ()
self.tree.insert("", "end",
values=(name, f"0x{value:04X}", is_set, desc),
tags=tags)
def _reset_results(self):
for var in (self.minor_version, self.major_version, self.cp_count,
self.access_flags_hex):
var.set("-")
self.tree.delete(*self.tree.get_children())
def main():
root = tk.Tk()
AccessFlagsViewer(root)
root.mainloop()
if __name__ == "__main__":
main()
运行结果
请将以上代码保存为 access_flags_viewer.py,使用如下命令可以运行它
bash
python3 access_flags_viewer.py
初始的效果如下图所示 ⬇️ (此时尚未选择 class 文件)

选择一个 class 文件后,就会展示这个 class 文件顶层的 access_flags。示例效果如下 ⬇️

补充说明
借助 javap 命令来查看 access_flags
可以借助 javap 命令来查看 class 文件的 access_flags。示例如下(假设我们已经有一个名为 Main.class 的文件了)
bash
javap -v Main.class
示例运行结果如下图所示 ⬇️

运行环境提示
- 运行脚本需要本机有 Python
- 如果您没有安装 Tkinter 的话,也需要安装一下才能运行本文提供的 Python 脚本