上一篇【第08篇】DirEntry 与 ZipEntry——目录和 JAR 包的读取实现
下一篇【第10篇】Classpath 整合——让 JVM 找到所有 class 文件
摘要
上一篇实现了两个"叶子节点"(DirEntry、ZipEntry),这一篇搞定两个"组合节点"。
CompositeEntry处理a.jar;b.jar这种多路径组合——它自己不读文件,而是把请求委派给子节点。WildcardEntry处理lib/*通配符——扫描目录找出所有 jar,然后……它就变成了 CompositeEntry。
本文有几个值得玩味的 Go 技巧:CompositeEntry 为什么定义成切片类型而不是结构体、filepath.Walk递归遍历怎么用、如何用filepath.SkipDir实现"不递归子目录"的精确控制。看完你会对 Go 的类型系统有更深的理解。
一、CompositeEntry:一个"自己不干活"的节点
CompositeEntry 的职责很特殊:它自己不读任何文件,只负责把请求转发给子节点。
【CompositeEntry 的委派机制】 客户端调用 compositeEntry.readClass("java/lang/Object.class") │ ▼ ┌──────────────────────────────────────────┐ │ CompositeEntry │ │ "D:\classes;lib\a.jar;lib\*" │ │ │ │ 自己不读文件!只负责遍历子节点 ↓ │ └───┬───────────────┬───────────────────┬──┘ │ │ │ ▼ ▼ ▼ ┌────────┐ ┌──────────┐ ┌─────────────┐ │DirEntry│ │ ZipEntry │ │WildcardEntry│ │D:\cls │ │ a.jar │ │ (又是组合!) │ └───┬────┘ └────┬─────┘ └──────┬──────┘ │ │ │ 找到?│ 找到?│ 找到?│ │ │ │ └──────────────┴───────────────────┘ │ 第一个成功的立即返回(短路)类型定义:切片而非结构体
这是本文最有意思的地方。看代码:
// ch02/classpath/entry_composite.gopackageclasspathimport("errors""strings")// CompositeEntry 由更小的 Entry 组成,直接定义成 []Entry 切片类型typeCompositeEntry[]Entry注意:这里不是type CompositeEntry struct { entrys []Entry },而是直接把切片类型定义成新类型!
为什么要这样?
// 方案 A:结构体(啰嗦)typeCompositeEntrystruct{entrys[]Entry}// 用的时候:self.entrys[i],遍历要 for _, e := range self.entrys// 方案 B:直接定义切片类型(简洁)✅typeCompositeEntry[]Entry// 用的时候:self[i],遍历直接 for _, e := range self方案 B 的好处:
| 好处 | 说明 |
|---|---|
| 代码简洁 | 少一层字段访问,self[i]比self.entrys[i]清爽 |
| 直接 range | for _, entry := range self直接遍历,不用.entrys |
| 可直接 append | self = append(self, entry)就能追加 |
| 语义清晰 | “CompositeEntry 就是一组 Entry”——类型定义直接表达了这个语义 |
重点:这是 Go 的一个强大特性——可以给任何已有类型(包括切片、map、函数)定义新类型,并为它添加方法。这在 Java/C++ 里是做不到的(你没法给
ArrayList加方法)。
// Go 允许给切片类型定义方法func(self CompositeEntry)readClass(classNamestring)([]byte,Entry,error){// 在方法里,self 就是一个 []Entry,可以直接遍历for_,entry:=rangeself{...}}构造函数:拆分路径列表
funcnewCompositeEntry(pathListstring)CompositeEntry{compositeEntry:=[]Entry{}// 按分隔符拆分成多个子路径for_,path:=rangestrings.Split(pathList,pathListSeparator){entry:=newEntry(path)// ← 注意:递归调用工厂函数compositeEntry=append(compositeEntry,entry)}returncompositeEntry}关键在这一行:
entry:=newEntry(path)// 递归调用!重点:注意这里是递归的。因为拆分后的每个子路径,又可能是目录、jar、或通配符。比如
-cp "classes;lib\*",拆开后classes是 DirEntry,lib\*又是 WildcardEntry(内部还是组合)。这就是为什么组合模式能表达任意复杂的嵌套结构。
【递归构造过程】 newCompositeEntry("classes;lib\\*;b.jar") │ │ strings.Split 按 ";" 拆分 ▼ ["classes", "lib\\*", "b.jar"] │ ├─ newEntry("classes") ──► DirEntry │ ├─ newEntry("lib\\*") ──► WildcardEntry │ │ │ │ 扫描 lib/ 目录 │ ▼ │ newCompositeEntry("lib\\a.jar;lib\\c.jar") │ ├─► ZipEntry(a.jar) │ └─► ZipEntry(c.jar) │ └─ newEntry("b.jar") ──► ZipEntryreadClass():遍历 + 短路
func(self CompositeEntry)readClass(classNamestring)([]byte,Entry,error){for_,entry:=rangeself{data,from,err:=entry.readClass(className)iferr==nil{// 找到了,立即返回(短路)returndata,from,nil}// 没找到,继续尝试下一个}// 全都找不到returnnil,nil,errors.New("class not found: "+className)}逻辑极其清晰:挨个问,谁找到了就用谁的。
【readClass 的短路返回】 遍历 [DirEntry, ZipEntry(a.jar), ZipEntry(c.jar)] │ ├─ 问 DirEntry: "有 java/lang/Object.class 吗?" │ └─ err != nil(文件不存在)→ 继续问下一个 │ ├─ 问 ZipEntry(a.jar): "有吗?" │ └─ err != nil(包里没有)→ 继续 │ └─ 问 ZipEntry(c.jar): "有吗?" └─ err == nil ✅ → 立即 return,不再往后问重点:这个"短路"行为正是类路径优先级的实现。谁在类路径里排在前面,谁就优先被使用——这就是双亲委派之外的另一层"谁先找到用谁"的机制。
String():拼接子路径
func(self CompositeEntry)String()string{strs:=make([]string,len(self))fori,entry:=rangeself{strs[i]=entry.String()}returnstrings.Join(strs,pathListSeparator)}把每个子 Entry 的字符串表示用分隔符拼起来,还原成原始的路径列表。
// 效果compositeEntry:=newCompositeEntry("classes;lib\\a.jar")fmt.Println(compositeEntry)// 输出:D:\proj\classes;D:\proj\lib\a.jar (已转成绝对路径)注意make([]string, len(self))提前分配好容量——这是 Go 的性能习惯,避免 slice 反复扩容。
完整代码
packageclasspathimport("errors""strings")typeCompositeEntry[]EntryfuncnewCompositeEntry(pathListstring)CompositeEntry{compositeEntry:=[]Entry{}for_,path:=rangestrings.Split(pathList,pathListSeparator){entry:=newEntry(path)compositeEntry=append(compositeEntry,entry)}returncompositeEntry}func(self CompositeEntry)readClass(classNamestring)([]byte,Entry,error){for_,entry:=rangeself{data,from,err:=entry.readClass(className)iferr==nil{returndata,from,nil}}returnnil,nil,errors.New("class not found: "+className)}func(self CompositeEntry)String()string{strs:=make([]string,len(self))fori,entry:=rangeself{strs[i]=entry.String()}returnstrings.Join(strs,pathListSeparator)}二、WildcardEntry:通配符的魔法
一个惊喜:WildcardEntry 就是 CompositeEntry
原书的实现有个巧妙之处:WildcardEntry 根本没有定义新类型!
// ch02/classpath/entry_wildcard.gopackageclasspathimport("os""path/filepath""strings")// 注意返回类型是 CompositeEntry!funcnewWildcardEntry(pathstring)CompositeEntry{baseDir:=path[:len(path)-1]// 去掉末尾的 *compositeEntry:=[]Entry{}walkFn:=func(pathstring,info os.FileInfo,errerror)error{...}filepath.Walk(baseDir,walkFn)returncompositeEntry}为什么可以这样?想想lib\*的本质:
【lib\* 的本质】 -cp "lib\*" │ │ 展开:扫描 lib 目录,找出所有 jar ▼ -cp "lib\a.jar;lib\c.jar;lib\e.jar" │ │ 这正是一个多路径组合! ▼ CompositeEntry([ZipEntry(a.jar), ZipEntry(c.jar), ZipEntry(e.jar)])通配符展开后就是一组 jar,天然就是组合。所以 WildcardEntry 不需要新类型——它的构造函数直接返回一个 CompositeEntry 即可。
重点:这是"透过现象看本质"的设计。通配符不是一种新的类路径形式,它只是"一组 jar"的简写。识别到这一点,实现就变得异常简单。
filepath.Walk:递归遍历目录
Go 标准库提供了filepath.Walk来遍历目录树:
funcWalk(rootstring,fn WalkFunc)error// WalkFunc 的类型typeWalkFuncfunc(pathstring,info os.FileInfo,errerror)error它的工作方式是:从 root 开始递归遍历所有文件和子目录,每遇到一个就调用一次你传入的回调函数。
【filepath.Walk 遍历顺序(深度优先)】 lib/ ├── a.jar ← 第1个访问 ├── c.jar ← 第2个访问 ├── sub/ ← 第3个访问(目录) │ └── f.jar ← 第4个访问 └── e.jar ← 第5个访问walkFn 回调:精确控制遍历行为
walkFn:=func(pathstring,info os.FileInfo,errerror)error{// 1. 出错就返回错误iferr!=nil{returnerr}// 2. 如果是子目录,跳过(不递归!)ifinfo.IsDir()&&path!=baseDir{returnfilepath.SkipDir}// 3. 如果是 .jar/.JAR 文件,创建 ZipEntry 加入组合ifstrings.HasSuffix(path,".jar")||strings.HasSuffix(path,".JAR"){jarEntry:=newZipEntry(path)compositeEntry=append(compositeEntry,jarEntry)}returnnil// 继续遍历}filepath.Walk(baseDir,walkFn)三个关键点:
关键点 1:错误处理
iferr!=nil{returnerr}遍历过程中可能出错(权限不足、文件被删除等),直接返回错误终止遍历。
关键点 2:用 SkipDir 实现"不递归"
ifinfo.IsDir()&&path!=baseDir{returnfilepath.SkipDir}这是精髓所在:
info.IsDir()—— 当前访问的是目录path != baseDir——不是根目录本身(根目录要进去遍历,子目录才跳过)- 返回
filepath.SkipDir—— 告诉 Walk:跳过这个目录,不要进去
【SkipDir 的效果】 lib/ ← path == baseDir,进入遍历 ├── a.jar ✅ 收集 ├── c.jar ✅ 收集 ├── sub/ ← IsDir && path != baseDir │ └── f.jar ❌ 跳过(SkipDir 生效,不进入 sub/) │ └── deep/ │ └── g.jar ❌ 跳过 └── e.jar ✅ 收集 结果:[a.jar, c.jar, e.jar] ← 不含子目录里的 jar重点:回顾上一篇文章说的——通配符
lib/*不递归子目录。这里用filepath.SkipDir精确实现了这个语义。注意这里用的是函数式编程风格:把一个匿名函数作为回调传给Walk,通过返回值控制遍历行为。
关键点 3:只认 .jar/.JAR
ifstrings.HasSuffix(path,".jar")||strings.HasSuffix(path,".JAR"){再次印证上一篇文章提到的规则:通配符只匹配 jar,不匹配 zip。
完整代码
packageclasspathimport("os""path/filepath""strings")// newWildcardEntry 创建通配符 Entry// 注意:返回类型是 CompositeEntryfuncnewWildcardEntry(pathstring)CompositeEntry{baseDir:=path[:len(path)-1]// 去掉末尾的 "*"compositeEntry:=[]Entry{}walkFn:=func(pathstring,info os.FileInfo,errerror)error{iferr!=nil{returnerr}// 跳过子目录(不递归匹配)ifinfo.IsDir()&&path!=baseDir{returnfilepath.SkipDir}// 只收集 .jar / .JAR 文件ifstrings.HasSuffix(path,".jar")||strings.HasSuffix(path,".JAR"){jarEntry:=newZipEntry(path)compositeEntry=append(compositeEntry,jarEntry)}returnnil}filepath.Walk(baseDir,walkFn)returncompositeEntry}三、四个 Entry 实现全景
到这一步,Entry 接口的四个实现全部完成:
【Entry 接口 + 4 种实现全景图】 ┌────────────────────────────────────────────────────────────┐ │ Entry 接口 │ │ readClass(className) ([]byte, Entry, error) │ │ String() string │ └────────────────────────────────────────────────────────────┘ ▲ ▲ ▲ ▲ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌────────────────┐ ┌──────────────┐ │DirEntry │ │ZipEntry │ │CompositeEntry │ │WildcardEntry │ │ struct │ │ struct │ │ []Entry │ │ (复用Composite│ │ │ │ │ │ (切片类型!) │ │ Entry类型) │ ├─────────┤ ├─────────┤ ├────────────────┤ ├──────────────┤ │ absDir │ │ absPath │ │ 子Entry切片 │ │ 扫描目录 │ │ │ │ │ │ │ │ 找jar │ ├─────────┤ ├─────────┤ ├────────────────┤ ├──────────────┤ │拼路径 │ │开zip │ │遍历委派 │ │filepath.Walk │ │读文件 │ │查文件 │ │短路返回 │ │SkipDir不递归 │ │ │ │读内容 │ │ │ │拼成Composite │ └─────────┘ └─────────┘ └────────────────┘ └──────────────┘ 叶子 叶子 组合 组合| 实现 | 类型定义 | 字段 | 关键标准库 | 核心逻辑 |
|---|---|---|---|---|
| DirEntry | struct | absDir | path/filepath、ioutil | 拼路径 + 读文件 |
| ZipEntry | struct | absPath | archive/zip、ioutil | 开包 + 遍历 + 读 |
| CompositeEntry | []Entry | (切片本身) | strings | 遍历委派 + 短路 |
| WildcardEntry | 复用CompositeEntry | — | path/filepath、os | Walk + SkipDir + 收集 jar |
组合模式的威力
现在回头看,组合模式的价值体现得淋漓尽致:
// 客户端代码:永远只需要这一行,不管类路径多复杂data,entry,err:=someEntry.readClass("java/lang/Object.class")// someEntry 可能是:// · 单个目录 → DirEntry 自己读// · 单个 jar → ZipEntry 自己读// · "a;b;c" → CompositeEntry 挨个问// · "lib/*" → WildcardEntry 转 CompositeEntry 挨个问// · "classes;lib/*" → CompositeEntry 里有 WildcardEntry,嵌套委派// 客户端完全不用关心!重点:这就是设计模式的价值——把复杂度封装在结构里,对外暴露简单的接口。以后要新增一种类路径形式(比如从 HTTP 远程加载类),只需要加一个
HttpEntry实现,现有代码一行都不用改。
本篇小结
本篇完成了 Entry 接口的两个组合节点:
CompositeEntry(多路径组合)
- 类型定义为
type CompositeEntry []Entry——直接给切片类型定义方法,这是 Go 的类型系统特性 - 构造函数用
strings.Split拆分路径,递归调用newEntry()创建子 Entry readClass()遍历子节点委派,第一个成功的立即返回(短路),实现了类路径优先级- 自己不读文件,纯委派
- 类型定义为
WildcardEntry(通配符)
- 不定义新类型——通配符展开后本质就是"一组 jar",直接返回 CompositeEntry
- 用
filepath.Walk递归遍历目录 - 用
filepath.SkipDir精确实现"不递归子目录"的语义 - 只收集
.jar/.JAR,不匹配 zip
设计思想
- 组合模式把"单个对象"和"组合对象"统一成同一接口
- 客户端代码不需要 if-else 判断类型,一行
readClass()搞定一切 - 新增类路径形式只需加一个实现类,零侵入
四个 Entry 实现全部就位。下一篇,我们把它们组装成完整的Classpath,让 JVM 真正具备"找到所有 class 文件"的能力——包括从 JDK 的 rt.jar 里加载 java.lang.Object。
上一篇【第08篇】DirEntry 与 ZipEntry——目录和 JAR 包的读取实现
下一篇【第10篇】Classpath 整合——让 JVM 找到所有 class 文件