从一个 Issue 到 Contributor:我给 java-design-patterns (94.1K+ star)添加 Thread-Specific Storage 模式的完整记录

最近,我在开源项目 iluwatar/java-design-patterns 中提交的 Thread-Specific Storage 模式已经被合并(从我有这个念头发现这个issue到被merge历时一年多了…),而我也因此被加入了项目的 contributor 列表。回头看,这并不是一次“写完代码就结束”的轻松提交,而是一段很完整的开源协作经历:从认领 issue、阅读规范、补齐 README 和 UML,到处理 AI reviewer、Spotless、SonarQube、CI 和后续 stale 状态,再到最后被 maintainer 合并并通过 all-contributors 机器人加入贡献者名单。

这篇文章想完整记录这个过程。一方面是给自己留个纪念,另一方面也想告诉后来者:一次真正的开源贡献,往往不只是“把功能写出来”这么简单。

这次贡献从哪里开始

这次对应的任务是 issue #3225,标题是 Implement Thread-Specific Storage Pattern

issue 的描述非常明确:需要在仓库中新增一个 thread-specific-storage 模块,用 Java 的 ThreadLocal<T> 演示 Thread-Specific Storage 模式,并满足几项验收标准:

  • 要有独立模块或包
  • 要有清晰的线程本地数据存取示例
  • 要有 README 或文档说明
  • 要遵守仓库代码风格
  • 要补上多线程场景下的单元测试
  • 要通过 CI 检查

这类 issue 很适合认真做一次完整的开源贡献。它不是只改一两行的小修小补,而是需要从“概念解释、代码实现、测试覆盖、文档表达、工程集成”几个维度一起完成。我当时刚好也在学习 Java 并发相关内容,而javaweb也常使用ThreadLocal作为线程上下文容器来存储用户变量,所以这个题目对我来说非常有吸引力。

我实际提交了什么

我最终提交的 PR 是 #3422,标题是 Add Thread-Specific Storage design pattern

从功能上看,这个 PR 做了几件事:

  • 在父 pom.xml 中注册了 thread-specific-storage 模块
  • 新增 UserContextUserContextProxyRequestHandlerApp 等核心类
  • ThreadLocal 保存线程隔离的用户上下文
  • finally 中显式清理线程本地变量,避免潜在内存泄漏
  • 编写了对应测试,覆盖线程隔离和清理行为
  • 补充了 README、示意图和 PlantUML 文件

如果只看最终合并结果,这像是一笔很干净的提交。但真正有意思的,是中间那条并不“线性”的演进路径。

时间线:这次开源贡献是怎么一步步推进的

下面这条时间线里,GitHub 页面时间我统一按 UTC 记录;本地 git 提交时间则保留了我机器上的 +08:00 时区。

1. issue 先于实现存在了很久

issue #3225 创建于 2025-03-30 09:06:07 UTC
它并不是我一出现就新开的任务,而是项目里已经存在了一段时间、也有多人关注过的 feature issue。在5月31日我第一个表达了想做这个的意愿。当时不懂,想的是一定要被assigned才能开做,于是就等呀等,甚至有了别人也留言要开始做。于是在9月26日,“If there’s no objection, I’ll start implementing this now and submit a PR. 🚀”。但当时又忙着上班,做完了初次提交就没有精力再做下去,直到次年的1月28日才想起来,做完交了pr。

这件事很能说明开源协作的一个现实:很多需求并不是没人想到,而是一直缺一个愿意把它真正做完的人。

2. 第一个核心提交:先把模式完整搭起来

我本地最关键的初始提交是:

  • 23f22a37d
    Add Thread-Specific Storage design pattern
    本地时间:2026-01-22 23:34:31 +08:00

这一次提交直接把模块骨架、源码、测试、README 和 UML 一起带上了。也就是说,我不是先丢一个“占坑 PR”,而是尽量把一个设计模式应有的教学形态一次性做完整。

对这种 tutorial 型仓库来说,这一点很重要。因为这里不是单纯追求“代码能跑”,而是要让读者能看懂这个模式是什么、为什么这么设计、在什么场景里使用。

3. 很快发起 PR,但很快也收到了第一波反馈

PR #3422 创建于 2026-01-22 15:50:43 UTC

PR 刚开不久,自动化 reviewer 就给出了第一批非常具体的反馈,主要集中在两点:

  • @Slf4j 下使用了 LOGGER,但 Lombok 默认生成的是 log
  • 在子线程里直接做断言,测试失败不一定能稳定反馈到主测试线程

我觉得这两条很有代表性。第一条是典型的“代码逻辑没错,但工程细节会让它编译不过”;第二条则是并发测试里非常常见、也很容易被忽略的问题。很多时候你以为自己写了测试,其实只是“看起来有测试”。

4. 开始进入“修格式、修静态检查、修测试稳定性”的阶段

接下来几次提交,基本上都不是功能扩展,而是在把这个 PR 往“可合并状态”推进。

  • 5d8581ac3
    Run 'mvn spotless:apply' to fix these violations.
    时间:2026-01-22 16:42:25 UTC

这次主要是在处理格式和风格问题。很多人第一次做开源会觉得这些检查“有点烦”,但其实这正是大型仓库维持一致性的方式。不是为了为难贡献者,而是为了降低后续维护成本。

  • 34cc03f6b
    fix: address Sonar security hotspots for Random usage and e.printStackTrace()
    时间:2026-01-22 17:30:20 UTC

这次主要是处理 SonarQube 的关注点,比如:

  • SecureRandom 替换 Random
  • e.printStackTrace() 改成日志输出

这里其实很能体现“开源仓库”和“本地练习代码”的差别。平时自己写 demo 时,这些地方很容易随手带过;但一旦要进成熟仓库,静态分析工具会逼着你把这些细节补齐。

  • 106f06d80
    Run 'mvn spotless:apply'(remove unused import)
    时间:2026-01-22 17:52:19 UTC

这又是一次很典型的“修小问题但必须修”的提交。你会发现,真正进入工程化流程后,很多合并前的工作都不是“难”,而是“细”。

5. 最有价值的一次重构:不仅修 bug,还把结构做得更干净

我认为中间最关键的一次迭代是:

  • d3a804105
    Refactor Thread-Specific Storage pattern implementation to address Sonar issues...
    时间:2026-01-23 03:27:24 UTC

这次不是单纯“修一个点”,而是整体上把实现打磨得更合理,包括:

  • 去掉了不必要的 UserContextProxy 实例化
  • 给工具类补上私有构造器
  • 用菱形语法简化泛型
  • 改善线程中断处理
  • 使用 Awaitility 改进测试等待方式,让测试不再依赖脆弱的 Thread.sleep
  • RequestHandler 的职责更收敛,使用静态 UserContextProxy 调用而不是携带冗余依赖

这次重构让我体会很深的一点是:
开源评审最有价值的地方,不是“帮你挑刺”,而是逼你把一个原本能工作的版本,继续打磨到更适合长期维护的状态。

6. 最后补 README 和 UML,让表达和实现一致

最后我又提交了:

  • 1bc4eddd8
    fixed: update uml and README.md
    本地时间:2026-01-23 12:20:07 +08:00

这一步很容易被低估。很多人做完代码就觉得任务结束了,但对 java-design-patterns 这种仓库来说,文档和图示并不是附属品,而是交付的一部分。

尤其是设计模式教程项目,读者通常是先看 README,再看图,再决定要不要深入读代码。所以 README 和 UML 不是“锦上添花”,而是这个模块的门面。

7. CI 还出过一次小插曲

我后来还专门在 PR 里留言说明:CI 可能因为 AI Reviewer workflow 的配置问题失败,所以我通过一次空提交触发了重新运行:

  • c110258184
    Trigger CI rerun
    时间:2026-01-23 04:35:39 UTC

这件事也很真实。开源贡献里,阻碍你前进的未必总是业务代码本身,有时候是 workflow、有时候是权限、有时候是第三方机器人。解决问题的能力,不只体现在写代码上,也体现在你能不能判断出“问题到底出在我这里,还是出在流水线环境里”。

8. 所有检查通过了,但 PR 并没有马上被合并

根据 SonarQube 在 PR 下的回报,这个 PR 最终达到了:

  • 0 New issues
  • 0 Security Hotspots
  • 86.0% Coverage on New Code
  • 0.0% Duplication on New Code

这说明从代码质量角度看,它其实已经达到了一个很健康的状态。

但现实是,通过检查不等于立刻合并

PR 在 2026-03-30 还被 bot 标记成了 stale。对第一次认真参与开源的人来说,这种阶段挺磨心态的:你会觉得“我明明已经做完了,为什么还没有结果?”

我后来慢慢理解了,maintainer 的节奏和贡献者的节奏并不总是一致。很多仓库 issue、PR 很多,维护者未必能第一时间处理到每一个贡献。这个时候最重要的能力,反而是耐心。

9. 最终合并,以及成为 contributor

好消息是,这个 PR 最终还是被合并了。

  • PR #3422 合并时间:2026-06-01 18:11:47 UTC
  • 对应 issue #3225 关闭时间:2026-06-01 18:12:57 UTC

更让我开心的是,在合并后不久,maintainer iluwatar 还在 PR 评论里触发了:

@all-contributors please add @CMD137 for code

随后机器人自动发起了 PR #3497,标题是 docs: add CMD137 as a contributor for code,并在 2026-06-01 18:13:50 UTC 被合并。

这意味着我不仅完成了这次贡献,也正式被写进了项目的 contributor 列表中。

对我来说,这个瞬间很有纪念意义。因为它意味着这次提交不再只是“我在本地完成过的一次练习”,而是真正进入了项目历史。

这次贡献里,我学到的并不只是 ThreadLocal

表面上看,这次做的是一个并发设计模式;但真正让我收获最多的,其实是开源工程层面的东西。

1. 教程型仓库的交付标准,和普通 demo 完全不同

在自己本地写一个 ThreadLocal 示例,只要能跑通就够了。
但在 java-design-patterns 这样的仓库里,你交付的是一份“别人会学习、会引用、会模仿”的材料。

所以你必须同时关心:

  • 代码是否能表达模式本身
  • 命名是否清晰
  • 测试是否稳定
  • README 是否适合教学
  • UML 是否和实现一致
  • 工具链是否全部通过

这是一种比“完成功能”更高一级的要求。

2. ThreadLocal 的重点不只是“存”,更是“清”

这次实现里我特别在意的一点,是在 finally 中调用清理逻辑。

很多 ThreadLocal 教程只会讲“每个线程有独立副本”,但在真实项目里,更关键的是你必须意识到线程可能被线程池复用。如果不清理,线程本地数据就可能把上一次任务的信息带到下一次任务中,甚至造成内存泄漏。

所以 Thread-Specific Storage 模式真正想传达的,不只是“线程隔离很方便”,而是:

线程隔离是有代价的,必须配套明确的生命周期管理。

3. 测试多线程代码,不能只靠“睡一会儿”

这次 PR 里,关于测试稳定性的反馈给我印象很深。

一开始很多写法其实都很常见:

  • Thread.sleep(...) 等线程执行完
  • 在子线程里直接断言

这些写法不是完全不能用,但很容易变成脆弱测试。后来通过改进等待方式、让断言结果更可控,我才真正意识到:并发测试本身也是一门技术。

4. 开源贡献不是一次“提交代码”,而是一次“持续响应”

这次经历里,我并不是发完 PR 就结束了,而是持续在做:

  • 读反馈
  • 改实现
  • 修格式
  • 处理 Sonar
  • 处理 CI
  • 补文档
  • 等待 maintainer 节奏

这让我越来越觉得,开源协作本质上是一种异步合作。
你提交的不只是代码,也是你是否愿意把一件事负责到底的态度。

如果让我总结这次经历

如果只用一句话总结,我会说:

这次贡献让我第一次真正体验到,什么叫“把一个功能做成项目愿意接收的样子”。

从 issue #3225 到 PR #3422,再到 contributor PR #3497,这条链路让我看到了一次完整开源贡献的真实形态:

  • 不是只写代码
  • 不是只会修 bug
  • 不是检查全绿就万事大吉
  • 而是从实现、表达、规范、工具链到协作节奏,全部都要跟上

现在回头看,最值得纪念的其实不只是“我被加进 contributor 列表了”,而是我开始更具体地理解了开源项目是怎么运转的,也更相信自己可以在这样的项目里持续贡献下去。

如果你也正准备做第一次像样的开源贡献,我很想说一句:
不要怕流程复杂。真正走完一遍之后,你得到的成长,通常远比那几百行代码本身更大。

参考链接

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇