news 2026/9/9 4:22:45

【DIY系列:Java虚拟机】第09篇:CompositeEntry 与 WildcardEntry——组合与通配符的魔法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【DIY系列:Java虚拟机】第09篇:CompositeEntry 与 WildcardEntry——组合与通配符的魔法

上一篇【第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]清爽
直接 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)// ← 注意:递归调用工厂函数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") ──► ZipEntry

readClass():遍历 + 短路

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 │ └─────────┘ └─────────┘ └────────────────┘ └──────────────┘ 叶子 叶子 组合 组合
实现类型定义字段关键标准库核心逻辑
DirEntrystructabsDirpath/filepathioutil拼路径 + 读文件
ZipEntrystructabsPatharchive/zipioutil开包 + 遍历 + 读
CompositeEntry[]Entry(切片本身)strings遍历委派 + 短路
WildcardEntry复用CompositeEntrypath/filepathosWalk + 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 接口的两个组合节点:

  1. CompositeEntry(多路径组合)

    • 类型定义为type CompositeEntry []Entry——直接给切片类型定义方法,这是 Go 的类型系统特性
    • 构造函数用strings.Split拆分路径,递归调用newEntry()创建子 Entry
    • readClass()遍历子节点委派,第一个成功的立即返回(短路),实现了类路径优先级
    • 自己不读文件,纯委派
  2. WildcardEntry(通配符)

    • 不定义新类型——通配符展开后本质就是"一组 jar",直接返回 CompositeEntry
    • filepath.Walk递归遍历目录
    • filepath.SkipDir精确实现"不递归子目录"的语义
    • 只收集.jar/.JAR,不匹配 zip
  3. 设计思想

    • 组合模式把"单个对象"和"组合对象"统一成同一接口
    • 客户端代码不需要 if-else 判断类型,一行readClass()搞定一切
    • 新增类路径形式只需加一个实现类,零侵入

四个 Entry 实现全部就位。下一篇,我们把它们组装成完整的Classpath,让 JVM 真正具备"找到所有 class 文件"的能力——包括从 JDK 的 rt.jar 里加载 java.lang.Object。


上一篇【第08篇】DirEntry 与 ZipEntry——目录和 JAR 包的读取实现
下一篇【第10篇】Classpath 整合——让 JVM 找到所有 class 文件


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:22:05

威胁防护管理化:从堆工具到体系化运营的关键路径

前阵子和一个做运维的朋友聊天,他说自己公司去年一口气上了三套安全设备,累计投入大几十万,结果年底一场勒索攻击照样瘫痪了两天业务。说实话,这种故事我听过太多次了。软件安全的威胁防护,难点从来不是“买什么工具”…

作者头像 李华
网站建设 2026/9/9 4:19:52

终端AI编程代理opencode实战:安装、模型配置与Skills全指南

说句实话,AI编程代理这两年冒出来一大堆,Claude Code、Codex CLI、Cursor,各有各的拥趸。但opencode能在这种竞争里站稳脚跟,而且热度一直往上走,靠的不是多聪明,而是把“终端里跑通AI干活”这件事做得特别…

作者头像 李华
网站建设 2026/9/9 4:18:10

博客系统测试报告实战:功能链路、JMeter压测与安全巡检

给博客系统写测试报告,听起来是个很没有存在感的活儿。我自己维护的博客系统跑了一年多,大多数时候打开后台看一眼文章列表、发一条评论,觉得“应该没问题”就算测试完了。真正让我改变想法的是某次改版,我把标签页的查询逻辑顺手…

作者头像 李华
网站建设 2026/9/9 4:17:30

Ubuntu下GitKraken v6.5.1安装配置实战:从下载到日常使用

简介:这份资源是GitKraken v6.5.1的Ubuntu安装包,面向需要图形化Git操作界面的开发者与团队,解决命令行Git学习成本高、分支合并和冲突处理不直观的问题。该版本针对Ubuntu 16.04及以上系统优化,是免费迭代中较为稳定的一代&#…

作者头像 李华
网站建设 2026/9/9 4:14:10

边缘AI计算芯片与本地推理部署实战:从NPU架构到INT8量化

1. 从云端到边缘:本地AI推理为什么被需要这几年做AI项目落地,我最大的感受是:大家最初都习惯把一切交给云端——训练在云端、推理在云端、数据也往云端送。但真当设备上了产线、进了机房、装到了路边,问题就一个个冒出来了。网络抖…

作者头像 李华
网站建设 2026/9/9 4:12:55

Agent Skill实战:用npx命令安装AI技能包,从验证到避坑全指南

最近调试Agent工作流的时候,我发现团队里越来越多人不再手动往项目里塞prompt模板,而是直接跑一行命令给AI助手装技能包。比如这两天社区讨论度不低的npx skill add dietrichgebert/ponytail,用一句话就把一个独立维护的skill拉进本地环境&am…

作者头像 李华