【DIY系列:Java虚拟机】第07篇:Entry 接口设计——类路径的“积木块“ 上一篇【第06篇】类路径classpath是什么鬼——JVM 怎么找到你的 class 文件下一篇【第08篇】DirEntry 与 ZipEntry——目录和 JAR 包的读取实现摘要类路径看起来是一坨复杂的东西——可能是目录、可能是 jar 包、可能是lib/*这种通配符、还可能是用分号串起来的一长串。怎么把这么乱的东西统一抽象答案是组合模式Composite Pattern把类路径想象成一堆可以嵌套的积木块不管它是单个目录还是一长串路径组合对外都暴露同一个接口。本文设计并实现这个接口——Go 语言里的Entry接口。你会看到接口的两个方法如何设计、newEntry()工厂函数如何根据路径特征智能创建对应实现、四种 Entry 实现各自的职责以及 Go 接口隐式实现这个特性在这里用得有多爽。一、组合模式把类路径想象成俄罗斯套娃先理解设计思路。回到上一篇文章的结论类路径可以是这些形式# 形式 1单个目录-cpD:\classes# 形式 2单个 jar 包-cplib\a.jar# 形式 3通配符目录下所有 jar-cplib\*# 形式 4多个路径用分隔符串起来每个又可以是上面任意一种-cpD:\classes;lib\a.jar;lib\*;lib\b.zip看出规律了吗形式 4 是前三种的组合——它里面每一项又可以是目录、jar、或通配符。这就是典型的树形结构【类路径的树形结构组合模式】 类路径: D:\classes;lib\*;lib\b.zip │ ▼ ┌─────────────────────────────────────────┐ │ CompositeEntry 组合节点 │ │ 负责把路径按分隔符切开逐个委派 │ └───┬──────────────┬──────────────────┬───┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────────┐ ┌─────────┐ │DirEntry │ │WildcardEntry │ │ZipEntry │ │(叶子节点)│ │ (组合节点) │ │(叶子节点)│ │ │ └───┬──────┬───┘ │ lib\ │ │D:\classes│ │ │ │ b.zip │ └─────────┘ ▼ ▼ └─────────┘ ┌───────┐ ┌───────┐ │ZipEntry│ │ZipEntry│ │a.jar │ │c.jar │ └───────┘ └───────┘组合模式的精髓不管是组合节点CompositeEntry、WildcardEntry还是叶子节点DirEntry、ZipEntry对外都实现同一个接口。调用方不需要关心自己拿到的是单个目录还是一长串组合统一调用readClass()就行。【组合模式的好处】 客户端代码 │ │ entry.readClass(java/lang/Object.class) │ ▼ ┌───────────────────────────────┐ │ Entry 接口 │ ← 客户端只认识接口 └───────────────────────────────┘ ▲ ▲ ▲ │ │ │ DirEntry CompositeEntry ZipEntry ← 具体实现对客户端透明 ❌ 不用组合模式 if 是目录 { ... } else if 是jar { ... } else if 是组合 { 递归判断... } 客户端代码里塞满 if-else加一种新类型就得改所有地方 ✅ 用组合模式 entry.readClass(name) // 一行搞定管你是什么类型重点组合模式让单个对象和组合对象具有一致的接口。这是设计可扩展系统的关键手法——以后要加一种新类型比如从网络 URL 加载类只需要新增一个 Entry 实现现有代码一行都不用改。二、Entry 接口的两个方法接口设计得极其精简——只有两个方法// ch02/classpath/entry.gopackageclasspathimportosimportstringsconstpathListSeparatorstring(os.PathListSeparator)// Entry 接口表示类路径中的一项typeEntryinterface{// readClass 寻找并读取 class 文件readClass(classNamestring)([]byte,Entry,error)// String 返回该项的字符串表示相当于 Java 的 toStringString()string}方法 1readClass()readClass(classNamestring)([]byte,Entry,error)逐个拆解部分说明参数classNameclass 文件的相对路径用斜线/分隔带.class后缀返回值 1[]byte读到的 class 文件字节数据返回值 2Entry最终定位到该文件的 Entry便于调试知道这个类是从哪个 jar/目录来的返回值 3error错误信息。找不到就返回 error参数格式举例要加载的类 传给 readClass 的参数 ───────────────────────────────────────────────── java.lang.Object → java/lang/Object.class com.example.Hello → com/example/Hello.class HelloWorld → HelloWorld.class java.lang.String[] → java/lang/String[].class 数组类第8章处理重点为什么参数用斜线/而不是反斜杠\因为这是JVM 内部的统一表示法跟操作系统无关。这样在 Windows 上也能统一处理避免了平台差异。转换工作/→\由各个 Entry 实现内部去适配。为什么要返回第二个 Entry这个设计很巧妙。想象一下调试场景data,entry,err:cp.ReadClass(java/lang/Object)fmt.Printf(java.lang.Object 加载自%v\n,entry)// 输出java.lang.Object 加载自C:\Java\jdk1.8.0_202\jre\lib\rt.jar一眼就能看出这个类是从 rt.jar 还是从你的 classes 目录加载的。排查类加载冲突同一个类被不同 jar 里的版本覆盖时这个信息非常有用。方法 2String()String()string相当于 Java 的toString()。Go 里只要实现了String() string方法用%v、%s格式化输出时就会自动调用它。fmt.Printf(%v\n,entry)// 自动调用 entry.String()fmt.Println(entry)// 同样自动调用pathListSeparator 常量constpathListSeparatorstring(os.PathListSeparator)这是路径分隔符跨平台的关键操作系统os.PathListSeparator值Windows;;Linux / macOS::用os.PathListSeparator而不是硬编码;代码就能自动适配不同系统。三、newEntry() 工厂函数有了接口还需要一个工厂来根据路径字符串创建对应的实现。这就是newEntry()// newEntry 根据路径特征创建对应的 Entry 实现funcnewEntry(pathstring)Entry{// 1. 包含路径分隔符 → 组合入口ifstrings.Contains(path,pathListSeparator){returnnewCompositeEntry(path)}// 2. 以 * 结尾 → 通配符入口ifstrings.HasSuffix(path,*){returnnewWildcardEntry(path)}// 3. 以 .jar/.JAR/.zip/.ZIP 结尾 → ZIP 入口ifstrings.HasSuffix(path,.jar)||strings.HasSuffix(path,.JAR)||strings.HasSuffix(path,.zip)||strings.HasSuffix(path,.ZIP){returnnewZipEntry(path)}// 4. 其他情况 → 目录入口returnnewDirEntry(path)}判断逻辑是个优先级递减的决策链【newEntry() 决策流程】 输入路径 path │ ▼ ┌─────────────────────────────┐ │ 包含分隔符 ( ; 或 : ) │──是──► newCompositeEntry └──────────┬──────────────────┘ │ 否 ▼ ┌─────────────────────────────┐ │ 以 * 结尾 │──是──► newWildcardEntry └──────────┬──────────────────┘ │ 否 ▼ ┌─────────────────────────────┐ │ 以 .jar/.JAR/.zip/.ZIP 结尾│──是──► newZipEntry └──────────┬──────────────────┘ │ 否 ▼ newDirEntry 兜底当成目录处理为什么要先判断分隔符因为-cp a.jar;b.jar这种组合路径虽然包含.jar但它整体是个组合必须先拆开。顺序反了就会把整串当成一个 jar 文件名。为什么目录是兜底选项因为目录路径没有明显的特征既没有*也没有.jar后缀。任何看起来不像其他的路径就当目录处理——这和现实中的直觉一致。举几个例子输入路径判断过程创建的实现D:\classes无分隔符、无*、无 jar 后缀DirEntrylib\a.jar无分隔符、无*、有 .jarZipEntrylib\*无分隔符、有 *WildcardEntryclasses;lib\a.jar有分隔符CompositeEntry.都不满足DirEntry当前目录四、四种 Entry 实现的职责Entry 接口有四个实现各司其职【四种 Entry 实现】 ┌────────────────────────────────────────────────────────────┐ │ Entry 接口 │ └────────────────────────────────────────────────────────────┘ ▲ ▲ ▲ ▲ │ │ │ │ ┌─────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────┐ │DirEntry │ │ ZipEntry │ │CompositeEntry│ │WildcardEntry│ ├─────────┤ ├────────────┤ ├──────────────┤ ├────────────┤ │目录形式 │ │JAR/ZIP文件 │ │多路径组合 │ │通配符 lib/* │ │ │ │ │ │ │ │ │ │字段: │ │字段: │ │字段: │ │字段: │ │absDir │ │absPath │ │entrys []Entry│ │(内部复用 │ │ │ │ │ │ │ │Composite) │ ├─────────┤ ├────────────┤ ├──────────────┤ ├────────────┤ │实现: │ │实现: │ │实现: │ │实现: │ │拼接路径 │ │打开zip │ │遍历子Entry │ │扫描目录 │ │读文件 │ │查找文件 │ │逐个委派 │ │找所有jar │ │ │ │解压读取 │ │第一个成功即用│ │再组合 │ └─────────┘ └────────────┘ └──────────────┘ └────────────┘ 叶子节点 叶子节点 组合节点 组合节点实现对应路径形式读取方式类型DirEntryD:\classes、.拼接绝对路径直接os.ReadFile叶子ZipEntrylib\a.jar、x.zip打开 zip 文件在其中查找并解压叶子CompositeEntrya;b;c按分隔符拆分逐个委派取第一个成功组合WildcardEntrylib\*扫描目录下所有.jar构造成 CompositeEntry组合后面三篇文章会逐个实现它们。这里先记住整体分工。五、Go 接口的隐式实现有多爽最后聊个 Go 语言的设计亮点接口是隐式实现的。在 Java 里实现一个接口必须写implements// Java必须显式声明publicclassDirEntryimplementsEntry{Overridepublicbyte[]readClass(StringclassName){...}}在 Go 里不需要任何声明——只要结构体实现了接口的所有方法它就自动是那个接口的实例// Go不需要 implements实现了方法就算typeDirEntrystruct{absDirstring}func(self*DirEntry)readClass(classNamestring)([]byte,Entry,error){// ...}func(self*DirEntry)String()string{returnself.absDir}// 上面两个方法一写*DirEntry 就自动实现了 Entry 接口vare EntryDirEntry{absDir:D:\\classes}// ✅ 编译通过重点这就是所谓的duck typing鸭子类型——“如果它走起来像鸭子、叫起来像鸭子那它就是鸭子”。Go 编译器在编译期检查方法集不需要运行时的instanceof。这个特性在解析 class 文件常量池时会发挥到极致——14 种常量各写一个结构体各自实现readInfo()方法然后统一放进[]ConstantInfo里管理完全不需要继承体系。完整代码entry.go把前面的内容整合起来ch02/classpath/entry.go的完整实现packageclasspathimport(osstrings)// pathListSeparator 存放路径分隔符// Windows 下是 ;Linux/Mac 下是 :constpathListSeparatorstring(os.PathListSeparator)// Entry 表示类路径中的一项typeEntryinterface{// readClass 寻找并读取 class 文件// 参数 className 是相对路径用斜线分隔带 .class 后缀// 返回值字节数据、找到该文件的 Entry、错误信息readClass(classNamestring)([]byte,Entry,error)// String 返回该项的字符串表示String()string}// newEntry 根据路径创建不同类型的 Entry 实例funcnewEntry(pathstring)Entry{// 包含分隔符 → 组合入口ifstrings.Contains(path,pathListSeparator){returnnewCompositeEntry(path)}// 以 * 结尾 → 通配符入口ifstrings.HasSuffix(path,*){returnnewWildcardEntry(path)}// jar 或 zip 文件 → ZIP 入口注意大小写ifstrings.HasSuffix(path,.jar)||strings.HasSuffix(path,.JAR)||strings.HasSuffix(path,.zip)||strings.HasSuffix(path,.ZIP){returnnewZipEntry(path)}// 其他情况 → 目录入口兜底returnnewDirEntry(path)}注意readClass和newEntry都是小写开头——在 Go 里这表示它们只在classpath包内可见。对外只暴露大写的Entry接口以及后面会实现的Classpath。这是 Go 的封装惯例最小化暴露的 API 表面。本篇小结本文完成了类路径的抽象设计组合模式类路径是树形结构用组合模式把单个路径和路径组合统一抽象成同一个接口Entry 接口只有两个方法——readClass(className) ([]byte, Entry, error)负责读取String()负责字符串表示参数格式class 名用斜线分隔、带.class后缀java/lang/Object.class这是 JVM 内部统一表示法newEntry 工厂按含分隔符 → 含*→ jar/zip 后缀 → 兜底目录的优先级链创建四种实现四种实现DirEntry目录、ZipEntryjar/zip、CompositeEntry多路径组合、WildcardEntry通配符Go 的隐式接口实现方法即实现接口无需implements后续解析常量池时会大量受益下一篇我们实现最简单也最基础的两个 EntryDirEntry和ZipEntry。它们负责真正把 class 文件从磁盘或压缩包里读出来。上一篇【第06篇】类路径classpath是什么鬼——JVM 怎么找到你的 class 文件下一篇【第08篇】DirEntry 与 ZipEntry——目录和 JAR 包的读取实现