
上一篇【第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(errorsstrings)// 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]清爽直接 rangefor _, entry : range self直接遍历不用.entrys可直接 appendself 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)// ← 注意递归调用工厂函数compositeEntryappend(compositeEntry,entry)}returncompositeEntry}关键在这一行entry:newEntry(path)// 递归调用重点注意这里是递归的。因为拆分后的每个子路径又可能是目录、jar、或通配符。比如-cp classes;lib\*拆开后classes是 DirEntrylib\*又是 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)iferrnil{// 找到了立即返回短路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(errorsstrings)typeCompositeEntry[]EntryfuncnewCompositeEntry(pathListstring)CompositeEntry{compositeEntry:[]Entry{}for_,path:rangestrings.Split(pathList,pathListSeparator){entry:newEntry(path)compositeEntryappend(compositeEntry,entry)}returncompositeEntry}func(self CompositeEntry)readClass(classNamestring)([]byte,Entry,error){for_,entry:rangeself{data,from,err:entry.readClass(className)iferrnil{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(ospath/filepathstrings)// 注意返回类型是 CompositeEntryfuncnewWildcardEntry(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)compositeEntryappend(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/.JARifstrings.HasSuffix(path,.jar)||strings.HasSuffix(path,.JAR){再次印证上一篇文章提到的规则通配符只匹配 jar不匹配 zip。完整代码packageclasspathimport(ospath/filepathstrings)// 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)compositeEntryappend(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 │ └─────────┘ └─────────┘ └────────────────┘ └──────────────┘ 叶子 叶子 组合 组合实现类型定义字段关键标准库核心逻辑DirEntrystructabsDirpath/filepath、ioutil拼路径 读文件ZipEntrystructabsPatharchive/zip、ioutil开包 遍历 读CompositeEntry[]Entry切片本身strings遍历委派 短路WildcardEntry复用CompositeEntry—path/filepath、osWalk 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()创建子 EntryreadClass()遍历子节点委派第一个成功的立即返回短路实现了类路径优先级自己不读文件纯委派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 文件