|
| 1 | +--- |
| 2 | +title: "JAR 文件读取原理:ZIP 协议与随机访问" |
| 3 | +date: 2026-04-12 00:00:00 +0800 |
| 4 | +categories: [java, jvm] |
| 5 | +tags: [jar, zip, classloader, random-access] |
| 6 | +description: "JAR 不就是磁盘上一个文件吗,getResourceAsStream 怎么能读到里面?答案在 ZIP 二进制协议和 Central Directory 索引——不需解压,直接寻址。" |
| 7 | +--- |
| 8 | + |
| 9 | +JAR 不就是磁盘上一个文件吗,那 `getResourceAsStream("config.properties")` 怎么能读到"里面"的东西? |
| 10 | + |
| 11 | +1. Table of Contents, ordered |
| 12 | +{:toc} |
| 13 | + |
| 14 | +# 读取 JAR 内资源的三种姿势 |
| 15 | + |
| 16 | +```java |
| 17 | +// 1. ClassLoader(最常见) |
| 18 | +InputStream is = MyClass.class.getResourceAsStream("/config.properties"); |
| 19 | + |
| 20 | +// 2. 直接操作 JarFile |
| 21 | +try (JarFile jar = new JarFile("myapp.jar")) { |
| 22 | + JarEntry entry = jar.getEntry("config/app.properties"); |
| 23 | + InputStream is = jar.getInputStream(entry); |
| 24 | +} |
| 25 | + |
| 26 | +// 3. NIO FileSystem(Java 9+) |
| 27 | +FileSystem fs = FileSystems.newFileSystem(Path.of("myapp.jar"), (ClassLoader) null); |
| 28 | +Path inside = fs.getPath("/config.properties"); |
| 29 | +String content = Files.readString(inside); |
| 30 | +``` |
| 31 | + |
| 32 | +为什么不能用 `new File("config/app.properties")`?因为 JAR 内的路径是虚拟路径,不存在于磁盘文件系统。 |
| 33 | + |
| 34 | +# 不解压怎么读?直接解析 ZIP 二进制字节 |
| 35 | + |
| 36 | +上面三种方式虽然 API 不同,但底层做的是同一件事:**不把 JAR 解压到磁盘,直接从文件内部定位并读取目标字节。** 怎么做到的?JAR 本质上就是 ZIP 格式,所以答案是按 ZIP 协议直接解析二进制字节。 |
| 37 | + |
| 38 | +## ZIP 文件结构 |
| 39 | + |
| 40 | +```plain |
| 41 | +[Local File Header + Data] ... [Central Directory] [End of Central Directory] |
| 42 | +``` |
| 43 | + |
| 44 | +关键是末尾的 **Central Directory(中央目录)**,它是一个索引,记录每个 entry 的: |
| 45 | + |
| 46 | +- 文件名 |
| 47 | +- 压缩方式(stored / deflated) |
| 48 | +- **在文件中的字节偏移量(offset)** |
| 49 | +- 压缩后/原始大小 |
| 50 | + |
| 51 | +## 读取过程 |
| 52 | + |
| 53 | +```plain |
| 54 | +1. open JAR 文件(一次 syscall) |
| 55 | +2. seek 到文件末尾,找到 End of Central Directory |
| 56 | +3. 解析 Central Directory,建立"文件名 → offset"内存索引 |
| 57 | +4. 需要某个 entry 时,直接 seek 到对应 offset |
| 58 | +5. 按压缩方式(通常 Deflate)解压那段字节,返回流 |
| 59 | +``` |
| 60 | + |
| 61 | +## 示意图 |
| 62 | + |
| 63 | +```plain |
| 64 | +myapp.jar |
| 65 | +├─ offset 0: [Local Header] [Main.class deflate 压缩数据] |
| 66 | +├─ offset 4096: [Local Header] [app.properties 数据] |
| 67 | +│ |
| 68 | +└─ offset 98304: [Central Directory] |
| 69 | + "com/example/Main.class" → offset=0, size=2048 |
| 70 | + "config/app.properties" → offset=4096, size=512 |
| 71 | +``` |
| 72 | + |
| 73 | +读 `config/app.properties`:查索引 → `lseek(fd, 4096)` → 读 512 字节 → inflate → 返回流。 |
| 74 | + |
| 75 | +# 本质类比 |
| 76 | + |
| 77 | +> **随机文件访问 + 内存索引**,和数据库 B-Tree 索引思路一样。 |
| 78 | +
|
| 79 | +不需要扫描整个文件,直接跳到目标位置读字节。ZIP 格式本身就是为随机访问而设计的——Central Directory 存在的意义就是支持这种"不解压、直接寻址"的访问模式。 |
| 80 | + |
| 81 | +JDK 的 `ZipFile` 底层用 C 实现(`zip_util.c`),`getEntry` 是 O(1) 哈希查找,`getInputStream` 做的是 seek + inflate。 |
0 commit comments