<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Pil0tXia</title>
  
  <subtitle>浮笙事记</subtitle>
  <link href="https://www.pil0txia.com/atom.xml" rel="self"/>
  
  <link href="https://www.pil0txia.com/"/>
  <updated>2024-09-05T18:06:00.000Z</updated>
  <id>https://www.pil0txia.com/</id>
  
  <author>
    <name>Pil0tXia</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>撒下种子，然后不期而遇 —— 在 CommunityOverCode Asia 2024 与热爱重逢</title>
    <link href="https://www.pil0txia.com/post/2024-08-29_community-over-code-asia-2024/"/>
    <id>https://www.pil0txia.com/post/2024-08-29_community-over-code-asia-2024/</id>
    <published>2024-08-29T00:02:00.000Z</published>
    <updated>2024-09-05T18:06:00.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="与热爱重逢"><a href="#与热爱重逢" class="headerlink" title="与热爱重逢"></a>与热爱重逢</h2><p>我早该意识到，自从我需要请假参加此次 CommunityOverCode Asia 大会起，我就已经在被裹挟着前进，再无大片空闲的时间琢磨博客的词句。</p><p>校招入职小红书后，我从第四天就开始主动接手需求。82 小红书日、超级码力启航营…… 接踵而至的活动与大小周的存在让我愈发珍惜周末的休息时光，但我也不愿丢下我的<strong>热爱的根源</strong>。</p><h3 id="我为什么能请假自费参会？"><a href="#我为什么能请假自费参会？" class="headerlink" title="我为什么能请假自费参会？"></a>我为什么能请假自费参会？</h3><p>对于绝大多数普通开发者来说，没有公司支持，与工作又没有直接相关，“请两天假 + 放弃周末，自费飞去另一个城市，只为参加一场会议”，可能会是一个难以接受的选项。</p><p>可对我来说，<strong>技术热情是我身为工程师的乐趣来源</strong>。</p><p>在业务部门，新人接手的总会是练手级别的 CRUD，底层的各个方法还是不同前辈在不同时期写下的。想要与它们对接，就像是想要用一种形状的俄罗斯方块，拼上 N 种不同形状的方块。</p><p>即使是这样繁复的 CV Job，我也在尝试从比较方块间的不同的过程中寻找些许乐趣。更何况是为我留下了美好记忆、激励我一路深入 Coding World 的 Apache 软件基金会所组织的活动呢。</p><p>自从参加了去年的 CommunityOverCode Asia 2023 大会后，我关注了国内各类活动整整一年。我一直认为，国内再无比 CommunityOverCode Asia 办得更<strong>富有技术氛围</strong>的峰会。</p><blockquote><p>下文将简称 CommunityOverCode Asia 为 CoCA。</p></blockquote><h3 id="Apache-软件基金会办的会有什么特殊之处？"><a href="#Apache-软件基金会办的会有什么特殊之处？" class="headerlink" title="Apache 软件基金会办的会有什么特殊之处？"></a>Apache 软件基金会办的会有什么特殊之处？</h3><p><strong>办会的可持续性在于声誉和资金</strong>。像亚马逊、谷歌等云厂商巨头，他们举办的峰会，在介绍先进企业级解决方案的同时，<strong>不会</strong>向你透露具体的技术实现细节。他们想要的是向没有自研基因的公司推销商业化产品，进而通过办会盈利。</p><p>参加这种会议，参会者的身份大多会<strong>从开发者向项目经理偏移</strong>，关注点将从 “我们想要实现这个功能，该怎么做”，变成 “我们需不需要这个功能，这个功能能带来什么”。</p><p>一场保持<strong>厂商中立性</strong>、不专注于推销商业产品和解决方案、几乎所有议题都在详细描述最新特性与技术、而且没有基金会注资的峰会，想要持续办下去，是极度困难的。但主办方 ALC Beijing 做到了，连续四年。</p><p>作为国内顶级的开源技术峰会，为了达到收支平衡，更多靠的是声誉，与各个已经习惯于反哺开源社区的大厂的赞助。尽管如此，每年也都会亏钱。好消息是，一年亏得比一年少了😄。</p><h2 id="与热爱者相遇"><a href="#与热爱者相遇" class="headerlink" title="与热爱者相遇"></a>与热爱者相遇</h2><p>每每参加这样的一场技术讨论，对我的视野与热情的帮助都是阶梯式的爬升。这种契机所带来的成长，是埋在代码中苦读所无法比拟的。</p><p><strong>技术不只是代码本身，还有那些做技术的人</strong>。投身于一件事情的人是会散发出光芒的，一群这样的人聚集在一起，那样的光芒吸引着我。能够认识大家，我很幸运。</p><p>如果说去年的参会经历是为我彻底打开了开源世界的大门，那我今年最大的感触便是 —— <strong>收获了自己在开源世界中无意间撒下的种子</strong>。</p><h3 id="只身来到会场，却得以结伴而行"><a href="#只身来到会场，却得以结伴而行" class="headerlink" title="只身来到会场，却得以结伴而行"></a>只身来到会场，却得以结伴而行</h3><p>踏入会场没有十步，就转角遇见爱 —— 撞上了去年与我一起参会的华为云 EventGrid 技术负责人<a href="../2023-08-28_community-over-code-asia-2023/#%E8%AE%B2%E5%B8%88%E5%AE%B4">王海军</a>老师。两人庆幸的笑容心照不宣般挤满脸庞。</p><p>此时的我预想不到，在我所认识的老伙计们之外，“不断被陌生人认出来、可我却不认得他” 的一天才刚刚开始。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_092408.webp","alt":"会场 - 杭州浙大森林会议中心"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_092642.webp","alt":"会场 - 签到处"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_093233.webp","alt":"Keynote 会场"}]</div>      <div class="gallery-items">      </div>    </div><p><img src="https://nl.static.pil0txia.com/post/coca2024/MYXJ_20240829001946514_save_ps.webp" alt="Apache EventMesh 社区王海军老师，特地用心梳了个背头"></p><blockquote><p>尽管我已经连续 4 天每天只睡 4 个小时了，尽管我在大会前一天落地杭州的那晚已是凌晨，来到会场的我依然精神抖擞。</p><p>原因与去年一样的纯粹：热爱，与循环正向激励。</p><p>（好孩子不要学我，合照时的眼睛会细得能双缝干涉😂</p></blockquote><h3 id="与自己撒下的种子相遇"><a href="#与自己撒下的种子相遇" class="headerlink" title="与自己撒下的种子相遇"></a>与自己撒下的种子相遇</h3><p>就在王海军老师回去准备下午的演讲内容后，一位小伙儿抱着打探的目光坐到我的旁边。他拿出手机，屏幕上是我在 GSoC-CN (Google Summer of Code) 群聊中发过的大会现场照片，却仍然一言不发，生怕认错了人。</p><p>我再清楚不过他的想法，因为我也曾经像他一样。</p><p>都已经是时常一起聊天的群友了，自然不必拘谨。于是我笑着报出他的网名 “牧生”，然后拍下了第一次线下面基的合照：</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/MYXJ_20240819003433674_save_ps-v2.webp" alt="姬世文，通过开源之夏与开源社区结识"></p><blockquote><p>合照时的他还只是 SpringCloud Alibaba Committer，现在已经是 Apache Hertzbeat (incubating) Committer 了。</p></blockquote><p>他告诉我，群里还有几位小伙伴正在前往会场的路上，“自从读过你去年的<a href="https://mp.weixin.qq.com/s/fbpgSd1Ex5v-cRI5I1vqCQ">那篇公众号文章</a>后就一直期待着今年的 CoCA 大会”。</p><p>于是，等到上午 Keynote 会场结束时，我把大家聚在了一起。他们挨个报出自己的 ID，那些 ID 我曾经无比熟悉，此时又显得陌生，令我重新审视起网名与具象的面容之间的逻辑联系。</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/Image_1721974076361_ps.webp" alt="牧生(姬世文), 我, zaki(陈子康), 风凌(魏鹏宇); 还有部分同学在做志愿者/被导师叫走吃饭/下午到"></p><hr><p>就在我与开源之夏的同学们一起拍下这张合影前，大门走进几位研究生模样的行色匆匆的学生（学生还是社畜，一眼就可以分辨出来），领头的那位一见到我就立刻大步向前、叫出我的名字、同时双手握住我的手，然后掏出手机，说 “我是你的博客的粉丝”，加上微信，留下一脸懵逼的我，再次行色匆匆地离开了。</p><p>会议结束前的那天晚上，我躺在酒店的床上梳理新好友的备注，点进他的朋友圈，发现这是一位大佬 ——“Seata’s Cloud-Native Evolution: A Roadmap from Deployment to Future Possibilities” 议题讲师、清华大学研究生、开源之夏中选者、Apache Seata (incubating) Committer 尹祥琨。</p><p><strong>“原来我的博客有粉丝啊？”</strong></p><p>躺在床上的我说出了与合影前的自己所说的一样的话。</p><h3 id="相遇、但其实是重逢？"><a href="#相遇、但其实是重逢？" class="headerlink" title="相遇、但其实是重逢？"></a>相遇、但其实是重逢？</h3><p>开源社区中的绝大多数沟通都发生在 Network 上，当你在线下与一位 “陌生人” 相谈甚欢时，<strong>你可能会突然意识到这个人就在自己的微信好友列表里</strong> w</p><img src="https://nl.static.pil0txia.com/post/coca2024/Screenshot_2024-07-26-14-40-51-546_com.tencent.mm_ps.webp" style="zoom: 25%;"><p>4 月时，我协助着 ALC Shenzhen 社区群的运营，看到 Apache Pulsar 深圳线下 Meetup 的预告，心想，“消息系统里终于出现能独立办 Meetup 的社区了”，于是主动联系了傅腾，由此结识。</p><p>在大会的 Apache Pulsar 展板前见到他时，谁也没认出谁。正好小红书在准备拿 Pulsar 换掉 RocketMQ；我的 Apache EventMesh 社区今年的演讲议题是吉利汽车的最佳实践，吉利为 EventMesh 的 Store 选用了 Pulsar，以支持与公司级基础设施相整合的异地集群数据同步，两人就这么聊了起来。</p><p>我谈起 Pulsar 即将在北京举办的 Meetup，傅腾疑惑，“你是从哪看到的呢？”</p><p>我回道：“ALC Shenzhen 群里 Pulsar 的一位 PMC 告诉我的，好像是叫傅腾。”</p><p>傅腾：“我就是傅腾”</p><p>（两人面面相觑，随后相视而笑 hh）</p><p>然后我就被留下，拍了张和展板站在一起的照片，被他发了上面那张朋友圈：</p><img src="https://nl.static.pil0txia.com/post/coca2024/MYXJ_20240819023007811_save_ps.webp" style="zoom: 15%;"><hr><p>好运与人际关系是连锁的。傅腾帮我叫住了走出 Pulsar 摊位没多远的乔发展。</p><p>来自吉利汽车的乔发展是今年 Apache EventMesh 社区 “Apache EventMesh the event grid helps build an event platform” 议题的讲师。</p><p>早在大会前一个多月，我就和其他两位 PMC 一起帮他的演讲 PPT 出谋划策、提供内容，但我还是第一次见到真人。</p><p>当然，先和展板拍一张合照：</p><img src="https://nl.static.pil0txia.com/post/coca2024/mmexport1721974064835.webp" style="zoom: 15%;"><p>我和他背靠着狭窄的环形走廊的栏杆聊了一整个中午，面前人来人往 —— 技术负责人所负责的永远不止技术。</p><p>在此期间，我被人认出来的 Travel 还在继续。除了我博客的粉丝之外，还有我帮忙答疑解惑 / 修改简历 / 选 offer / 谈心的全国各地的同学们。</p><p>聊天的队伍壮大着。直到下午的分会场即将开始时，一群人才依依不舍地各自奔向自己最感兴趣的领域所在的会议室☺️。</p><h3 id="每个纪念品都记录着一次相遇"><a href="#每个纪念品都记录着一次相遇" class="headerlink" title="每个纪念品都记录着一次相遇"></a>每个纪念品都记录着一次相遇</h3><p>收集印有<strong>各个项目 Logo 的纪念品</strong>也是一种乐趣。</p><p>纪念品通常不是无限领取的，摊位会希望你扫码关注公众号、填个问卷之后，再选一个纪念品带走。</p><p>但如果你像我一样，上来就与工程师攀谈起来，在了解项目的用途与亮点之后，结合自己对技术应用的理解，提出些许改进的建议，拓宽项目落地特定场景的可能性，他们会非常乐意把所有种类的纪念品都给你一份 —— 多要几份作 Backup 也没事😉。</p><img src="https://nl.static.pil0txia.com/post/coca2024/Image_1722061990005.webp" alt="姬世文偷拍的扫货中的我" style="zoom: 20%;"><p>一圈摊位逛下来，也一定程度上弥补了自己无法听遍所有分会场的遗憾。看着自己收集的纪念品，回想起将它们收入囊中时的自己，不禁嘴角带笑。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_093127.webp","alt":"正价票赠送的大会周边礼包"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_145927.webp","alt":"Day1 纪念礼品"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_150612.webp","alt":"我所参与过的各个社区的 Logo 贴纸"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_145950.webp","alt":"木兰开源协议的纪念品最为精致"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240727_183130.webp","alt":"Day2 纪念礼品"},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240728_135602.webp","alt":"RocketMQ 展台直接没有人驻场 hh 君子局"}]</div>      <div class="gallery-items">      </div>    </div><hr><p>但当我遇到比自己还 e 的人时，我也只好拿上礼品逃离 ——</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/FV9A3963-opq3126667010.webp" alt="展板右侧的是李梦"></p><p>在 OSPP 开源之夏的摊位前，我碰上了开源之夏的发起人之一：李梦，简短地与她分享了我和开源之夏的故事。谁知也许是我身上的 buff 太多，“课题结束后持续贡献”、“深度参与 &amp; 建设社区”、“毕业前被提名 Apache Committer”、“双非本科”、“因开源经历拿到大厂 ssp”、“经常线上线下分享参与开源活动的经验”……</p><p>这些我一步步走来、自然而然的事情，在她眼里闪着光，如获至宝般邀请我作客杭州近期的一场线下分享。</p><p>不过分会场议题开讲在即，时间安排和行程方面，我的脑子一时间也转不过来，便回道 “乐意之至”，加上微信。盛情难却，原本已经领了一件她觉得 “正好” 的 L 码 T 恤，又和身边的姬世文一起接过两件更大一码的 T 恤，见她还要拿第五件，匆忙趁新客到场之时礼貌道别🤣。</p><p>能真切地感受到她对开源之夏的用心运营，也非常感谢对我的热情 w，期待可以继续与开源之夏的故事🤗。</p><p>（虽然 XL 也小了一码就是了😂第二天再去 OSPP 摊位时已经没有 2XL 的码了</p><h2 id="与-RedHat-的奇妙缘分"><a href="#与-RedHat-的奇妙缘分" class="headerlink" title="与 RedHat 的奇妙缘分"></a>与 RedHat 的奇妙缘分</h2><p>要说去年的 CoCA 大会给我留下<strong>最深刻印象</strong>的讲师，那必定是来自台湾 RedHat 的 <a href="../2023-08-28_community-over-code-asia-2023/#%E5%AD%A6">Luke Chen</a> 老师。可惜 “Deep Dive the replication protocol in Kafka” 这场议题之后，我就赶往下一个分会场了，没能与老师交流一番。</p><p>今年的我和 RedHat 继续着缘分。正当我在消息系统会场研究着<a href="../2023-08-28_community-over-code-asia-2023/#%E7%94%A8">赵福建</a>（也是我去年非常喜欢的讲师，这回讲的是 Serverless Kafka）的 PPT 时，乔发展把我叫去了位于 Room 8 的 “其它 (Others)” 分会场 —— 这么一个不起眼的房间竟然传出 Luke Chen 般同样理性叙述的声音，不出意外还真是 RedHat 的会场，着实惊喜。</p><p>（建议 CoCA 官网的会议日程页面可以借鉴去年百格活动的议题和嘉宾介绍排版，这样听众可以看到更醒目的公司背景，不必挨个点开演讲者的个人介绍，不容易错过。）</p><p>“The best pratice of integration technology driven by event in cloud” 这场议题介绍了 OpenShift 生态下独具一格的事件驱动架构，也很惊喜在演讲中提到了 Apache EventMesh 作为参考。</p><p>那既然是熟悉的领域，答题环节我必然势在必得，斩获全场唯一的 Special 纪念品：</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_153215.webp","alt":""},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_153246.webp","alt":""},{"url":"https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_153301.webp","alt":""}]</div>      <div class="gallery-items">      </div>    </div><p>本场议题结束后正好是茶歇时间，我抓住机会，主动上前与讲师 Alan Liu 老师交谈，请教他对于 EventMesh 的看法和建议。他也很高兴能见到 EventMesh 社区的维护者，期望与社区寻求合作。</p><p>说实话，第一次与 RedHat Senior Solution Architect 对话，再一次印证了我对 RedHat Engineer “不说废话” 的刻板印象，需要非常专心才能跟上对方的语速。</p><p>我叫回乔发展，四个人站成一圈，边吃茶歇边聊，从如何帮助开源项目得到商业大公司的背书与开发人力支持，到 Monolith 视角下 Serverless Store 的 StreamAPI Dispatch Runtime 优化，收获颇丰。</p><p>大会结束的那天晚上，我在回北京的航班的廊桥前拍飞机，又遇上了 Alan Liu 老师🤭。真是奇妙的缘分。</p><hr><p><img src="https://nl.static.pil0txia.com/post/coca2024/FV9A4388-opq3128747866.webp" alt="但茶歇总共就这么多"></p><p>我连续两天连块哈密瓜都差点没抢到。全靠姬世文帮我留了一份，才能在大会的最后一天吃上小蛋糕。</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/Image_1722153300281_ps.webp" alt="今年的点心比去年的更精致了，蛋糕上的花居然还是真花捏😋"></p><h2 id="晚宴：从聆听者到输出者"><a href="#晚宴：从聆听者到输出者" class="headerlink" title="晚宴：从聆听者到输出者"></a>晚宴：从聆听者到输出者</h2><p><strong>世界很小，在这里能碰到的熟人比你想象中的多。</strong></p><p>即使会议第一天的晚宴坐下了一桌陌生人，即使大多数人原本互相都不认识，等到晚宴结束的时候，就都认识了。</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/IMG_20240726_184250.webp" alt="晚宴会场"></p><p><img src="https://nl.static.pil0txia.com/post/coca2024/mmexport1722006290321_ps.webp" alt="晚宴后一桌人的合照 我左手边的是陈政羽和代立冬老师"></p><p>更何况，坐你身边的 “陌生” 人很有可能就与你有着 “不见之缘”——</p><p>我和王海军老师落座后，一桌人分成小群体窃窃私语的局面很快被我<strong>在不经意间</strong>打破。我与王海军老师聊到自己在公司 “忙到没有人会思考如何卷别人” 的体验，结果一则我 “在洗手间里有史以来听到过的唯三句话” 逗笑了全桌人：</p><ul><li>好累</li><li>比我在快手的时候累多了</li><li>啊干不动了 ——</li></ul><p>身边一位<strong>沉稳的中年开发者</strong>抓住了这个机会，提议我们自我介绍一圈。首当其冲的自然是我。</p><p>当我能够<strong>下意识地</strong>在一桌平均年龄比我大 10 岁的前辈面前自然而又饱含热情地介绍自己时，我意识到，今年的我自己，成长了。</p><p>来自各大企业的 Apache 项目开发者居多，Committer 也不少，但相比于去年的<strong>最大的变化</strong>在于：大会还能吸引到仅仅只是 “好奇开源” 或是 “想要捐献项目” 就来参会的<strong>开源圈外人</strong>，甚至还有市占率超 50%、SSD 卖到 HDD 价格的存储硬件供应商。</p><p>大家的自我介绍继续着，一位刘海贴着脑门、看上去总让人觉得在嘿嘿笑着的青年的<strong>声线</strong>吸引了我的注意。Flink 社区的归属更是让我确信了这一点：<strong>好像是熟人</strong>。直到他报出自己的大名，我知道我们是如何相识的了：</p><p>“你是不是 5 月份在大湾区 Open Source Day 演讲了货拉拉的 Flink CDC 实践？” 我准确报出自己看过的录播标题。</p><p>他稍有些惊讶地说 “啊，是”，桌上的大家也好奇地等着我继续说下去。</p><p>“我帮你的 PPT 找出了个小错误（笑），当时让 Track Chair 帮忙反馈给你了。”</p><p>“啊，对的，我知道这个”，他一拍脑门、笑着低下头去，一桌人也跟着我俩笑起来。</p><p>陈政羽老师（的声音）陪着我度过了三顿晚饭。每到没有条件专心 Review PR 的时候，我就会打开他的演讲，从上次停下的地方继续，直到从头到尾听完。</p><p>最后轮到这位提议自我介绍的 “<strong>沉稳的中年开发者</strong>”。他开始报出自己的 title——</p><p>白鲸开源联合创始人、Apache DolphinScheduler PMC Chair、Apache SeaTunnel PPMC、ASF Member、Apache 孵化器导师……</p><p>“好长！” 我心想，怪不得你把自己放到最后说。</p><p>代立冬，我有印象的。他的 title 远不止说出口的这么长，他不仅是 ApacheCon 2022 和 CoCA 2023 的大数据领域 Track Chair，还是最早主导国人开源项目成为 Apache 顶级项目的开源布道师。他穿着过曝的白 T 恤、双手抱在胸前的职业照，我已看过数遍。只是… 真人和照片实在是大相径庭，所以没认出来😂。</p><p>得益于我在签三方后到毕业前这段时间全职开源、带团队做产品的积累，我不再需要像去年的晚宴一样等待<strong>自己能与他人平等交换故事</strong>的话题，而是可以在整场晚宴中与每个人相谈甚欢。</p><p>从聆听者到输出者，我发现我并不是为了聊天而健谈。就像在 Code Review 时洋洋洒洒地写下一整页指引和想法一样，我借此学习，也乐意分享。</p><h2 id="离别，不是结束"><a href="#离别，不是结束" class="headerlink" title="离别，不是结束"></a>离别，不是结束</h2><h3 id="不断刷新的去年今日"><a href="#不断刷新的去年今日" class="headerlink" title="不断刷新的去年今日"></a>不断刷新的去年今日</h3><p>大会的最后一天，最后一场议题后，我追上 Justin Mclean，向他展示了我<a href="../2023-08-28_community-over-code-asia-2023/#%E8%AE%B2%E5%B8%88%E5%AE%B4">去年写的博客</a>：</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/Pasted%20image%2020240828023812.webp" alt="去年的 CoCA 上与 Justin Mclean 的合影"></p><p>他很高兴能重逢，也很高兴我这么年轻就已经成为了 Committer。</p><p>不过，他紧接着补充道，“虽然不知道是谁，但我相信 ASF 中还会有更年轻的 Committer”，一脸无比期待新鲜血液加入的表情。</p><p>然后我们就一起拍下了这张照片：</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/IMG_20240728_172733_ps.webp" alt="Craig 也加入进来了"></p><p>去年的博客，和今年的照片。</p><p>只可惜今年<a href="../2023-08-28_community-over-code-asia-2023/#%E8%AE%B2%E5%B8%88%E5%AE%B4">明哥</a> (EventMesh PMC 薛炜明) 和<a href="../2023-08-28_community-over-code-asia-2023/#%E8%AE%B2%E5%B8%88%E5%AE%B4">陈老板</a> (EventMesh PMC Chair 陈广胜) 没有来。去年一起去了天安门，今年要不去趟西湖？等到 CoCA 在深圳举办的时候，怎么说也得去找他们蹭一顿😬</p><p>于是，当姬世文怯生生地找到我，问我怎样才能与 Justin Mclean 英语对话、他也想合影的时候，我把他拽到了即将离场的 Justin Mclean 面前，帮他实现了<strong>第一次</strong>合影。</p><p>相信等到明年的时候，他已经学会了如何问好，并且能自己向 Justin Mclean 介绍：“我已经是 Commmitter 了”。</p><h3 id="每次打开电脑时，都请回想起那份热爱"><a href="#每次打开电脑时，都请回想起那份热爱" class="headerlink" title="每次打开电脑时，都请回想起那份热爱"></a>每次打开电脑时，都请回想起那份热爱</h3><p>回北京后，我就立刻为电脑贴上了那些我参与过的社区 Logo：</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/BRL_3844-opq3152384102_ps.webp" alt="紧接着就是公司的校招培训（手搓 xhs demo），第二次当 TL 了，满载而归"></p><p>在去年的大会上，每种贴纸我都只领了<strong>一份</strong>，等到想贴的时候才意识到：贴了就再也没剩的了；</p><p>今年我特意每种都拿了<strong>两份</strong>，贴一份收藏一份😙</p><p>不过，实用物品一直被收藏着也是在浪费它的生命，不如用起来，比如这个 Apache Paimon 水杯：</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/IMG_20240815_234105.webp" alt="和去年一样，这张照片也拍摄于晚上12点，热爱不减"></p><h2 id="从办会的见证者到-ALC-Beijing-Member"><a href="#从办会的见证者到-ALC-Beijing-Member" class="headerlink" title="从办会的见证者到 ALC Beijing Member"></a>从办会的见证者到 ALC Beijing Member</h2><p>我曾在<a href="../2023-08-28_community-over-code-asia-2023/#ALC-Shenzhen">去年的博客的 ALC 章节</a>中写到，“<em>2024 年，CommunityOverCode Asia 将来到深圳或杭州，吸引更多本地的开发者。</em>” 那时的我还不完全明白 ALC 的运营方式与职能。</p><p>在这一年里，无论是帮我及时认识<strong>各个项目的新特性与它们的开发者</strong>，还是为那些<strong>极易被社交媒体流量所埋没的 Meetup 与讲师</strong>提供曝光的机会，ALC Shenzhen 社群的存在都功不可没。</p><p>ALC (<a href="https://cwiki.apache.org/confluence/display/COMDEV/Apache+Local+Community+-+ALC">Apache Local Community</a>，类似谷歌 GDG) 本地社群中<strong>聚集了来自各个 Apache 项目的 PMC 级人物</strong>，是各种开源活动<strong>最好的门户</strong>，也是一张<strong>横跨数十个项目</strong>的人际关系网。六度分隔理论在这里的数量级将降级到三度以下 —— <strong>只需三步，你就可以认识到开源圈子中的任何人</strong>。</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/mmexport1722098113109_ps.webp" alt="晚宴在场的ALC Shenzhen社区成员 我右手边的刘文涛老师是我见过最年长的一线开发者"></p><p>ALC 社群中的活动可以自由发布，ALC Chapter 自身也可以举办活动，通常由 ALC Chapter Members 领导。</p><p><a href="https://cwiki.apache.org/confluence/display/COMDEV/ALC+Beijing">ALC Beijing</a> 作为国内最早建立的 ALC，最为大佬云集。CoCA 大会也是由 ALC Beijing Members <strong>十数人</strong>花费大半年时间<a href="https://cwiki.apache.org/confluence/display/COMDEV/ALC+Beijing+Team+meeting">一手筹划</a>的。</p><p>我从去年大会结束后就一直想着，<strong>给自己留下那么深好印象的一场大会，是怎么筹办、怎么收尾的呢？</strong>以及，他们为什么要<strong>无偿地举办</strong>这样一场大会呢？</p><p>于是我从去年开始就老找炜明 (EventMesh PMC) 要腾讯会议号，因为他说会议号是发在 ALC Beijing Members 的小群里的，而双周例会续期后，会议号就变了。有时我看 PMC 忙，就待在 ASF Confluence 页面上刷新，等江波写下今天的会议纪要，然后拿到此次会议的链接。</p><p>现在看来，大家几乎一整年都在办会。从去年 9、10 月份的复盘，到 11 月就开始筹划下一场了，再到今年的大会前夕。跟着一场场例会一路走来，我也从<strong>参会者</strong>，转变为了办会的<strong>见证者</strong> ——</p><p>并终于在今年的大会结束后，也成为了 ALC Beijing Member，正式成为他们中的一员。（不用找人要会议号了，耶😆）</p><p><img src="https://nl.static.pil0txia.com/post/coca2024/Pasted%20image%2020240829160055.webp"></p><p>非常感谢代立冬老师的提名与邀请！费心思帮我撰写投票邮件、发起投票，还在百忙之中让我在相应的时间节点后提醒他来推动，聊天时又从来没有任何架子，有一种无心栽柳柳成荫的感动。</p><p>代立冬老师问我，能不能总结一段我为开源做的贡献，他发给 ALC Beijing 的大家看下。</p><p>我不擅长夸自己，所以我一直憋到回北京的航班落地前才想出思路。<strong>尽管如此，我还是很喜欢我所写下的最后一段话：</strong></p><blockquote><p>在这场杭州之旅中见到很多位我的博客的粉丝，有刚刚参与工作的同学，也有讲师，感到很惊喜，想要在更多的开源活动中与自己撒下的种子相遇。</p></blockquote><p>“<strong>想要在更多的开源活动中与自己撒下的种子相遇</strong>”，这便是我走下飞机时心中最深的感想。</p><h2 id="尾声"><a href="#尾声" class="headerlink" title="尾声"></a>尾声</h2><blockquote><p>有趣的故事还有很多，但受限于篇幅，真的已经写不下了。</p><img src="https://nl.static.pil0txia.com/post/coca2024/IMG_20240728_213551.webp" alt="美丽的城市承载美好的记忆 我还会再来的" style="zoom: 33%;"></blockquote><p>感谢一切相遇。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;与热爱重逢&quot;&gt;&lt;a href=&quot;#与热爱重逢&quot; class=&quot;headerlink&quot; title=&quot;与热爱重逢&quot;&gt;&lt;/a&gt;与热爱重逢&lt;/h2&gt;&lt;p&gt;我早该意识到，自从我需要请假参加此次 CommunityOverCode Asia 大会起，我就已经在被裹挟着前进，</summary>
      
    
    
    
    <category term="生活" scheme="https://www.pil0txia.com/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="CommunityOverCode" scheme="https://www.pil0txia.com/tags/CommunityOverCode/"/>
    
    <category term="ALC" scheme="https://www.pil0txia.com/tags/ALC/"/>
    
  </entry>
  
  <entry>
    <title>Apache EventMesh vs. AWS EventBridge: “产品化” 是开源中间件的出路吗？</title>
    <link href="https://www.pil0txia.com/post/2024-06-12_os-or-commercial-product/"/>
    <id>https://www.pil0txia.com/post/2024-06-12_os-or-commercial-product/</id>
    <published>2024-06-12T10:32:51.000Z</published>
    <updated>2024-06-12T11:13:51.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文由 <a href="https://mp.weixin.qq.com/s/67od28RReSLKDBeDpQOzdg">Apache EventMesh</a> 公众号转载。</p></blockquote><h2 id="AWS-EventBridge-是-Apache-EventMesh-的理想形态吗？"><a href="#AWS-EventBridge-是-Apache-EventMesh-的理想形态吗？" class="headerlink" title="AWS EventBridge 是 Apache EventMesh 的理想形态吗？"></a>AWS EventBridge 是 Apache EventMesh 的理想形态吗？</h2><p>在 5 月底的亚马逊云科技峰会上，80% 的技术议题的项目架构都使用了 AWS 的 EventBridge。EventBridge 在 AWS 的生态中已经与 Serverless 概念和 Lambda 云函数几乎完全绑定。</p><p><img src="https://static.pil0txia.com/picgo/IMG_20240530_144804.webp" alt="EventBridge 解耦微服务与函数计算"></p><p>开源的 <a href="https://github.com/apache/eventmesh">Apache EventMesh</a> 可以从中学到些什么？我与在场的 SkyWalking 的<a href="https://github.com/wu-sheng">吴晟</a>老师就此问题进行了一些讨论，感谢吴晟老师的启发。</p><h2 id="事件驱动消息总线还有更广的应用场景"><a href="#事件驱动消息总线还有更广的应用场景" class="headerlink" title="事件驱动消息总线还有更广的应用场景"></a>事件驱动消息总线还有更广的应用场景</h2><p>EventMesh 适用于具有跨部门传输事件的需求的用户，这部分用户数量注定是很少的。想要拓展其应用场景，要么打破产品自身的定位（例如去做事件聚合等 ETL 的能力），要么只能接受这个现状（并不是妥协）。</p><p><img src="https://static.pil0txia.com/picgo/image-20240612004155011.webp" alt="哔哩哔哩直播行为系统"></p><p>微服务拆得越碎，越需要事件驱动。<a href="https://mp.weixin.qq.com/s/XCWM1OFDKXBGxa7TebrrzA">哔哩哔哩直播榜单</a> 这个场景的应用就挺好的。</p><p><img src="https://static.pil0txia.com/picgo/image-20240612004346549.webp" alt="异步事件聚合"></p><p>事件聚合功能将在 Filter 和 Transformer 实现可编排能力后，再考虑开发。</p><h2 id="EventMesh-和-EventBridge-不在一个赛道"><a href="#EventMesh-和-EventBridge-不在一个赛道" class="headerlink" title="EventMesh 和 EventBridge 不在一个赛道"></a>EventMesh 和 EventBridge 不在一个赛道</h2><p>EventMesh 是开源项目，EventBridge 是商业服务。使用 EventMesh 这样的开源项目就意味着企业需要雇一个人来运维、背锅，非互联网行业的企业一旦选择了云厂商就不会选择需要自行维护的开源项目。</p><p>因此，EventMesh 没有必要和云厂商的全托管方案去比较、模仿、竞争，但是可以从降低用户的使用和运维成本方面下手，来吸引更多 Subscriber（对项目感兴趣的人，包括可以拍板做 POC 的管理层，和执行 POC 的开发者）。</p><h2 id="控制台不用追求“产品化”"><a href="#控制台不用追求“产品化”" class="headerlink" title="控制台不用追求“产品化”"></a>控制台不用追求 “产品化”</h2><p>在 Apache EventMesh Dashboard 方面，半托管（接入并管理 EventMesh 集群的 Connection 为主）比全托管方案（在 K8s 中创建、管理 Store 集群和 Meta 集群）更适合开源项目。</p><p>云厂商的服务使用单一的环境，但你不知道开源项目的用户会怎么用。以 EventMesh Dashboard 管理 K8s 集群为例，用户可能会开启 TCP Mesh, CNI, OpenShift……Dashboard 是没法全部兼顾的。</p><p>即使全部支持并长期维护，这也并不是全托管，因为 K8s 集群并不是 Dashboard 托管的。如果 Dashboard 连 K8s 集群都需要 exclusive 专用的，那就挤占了用户的物理机资源，也不容易做弹性。开源项目没有能力维护这样的一个 DevOps 平台。</p><p>再者，大多数目标用户也不关心 EventMesh Dashboard 能不能管理 Store 和 Meta 的集群。在云厂商的平台上，用户需要自行创建不同服务的实例，再将它们关联起来；各个服务的管理页面也是独立的。对 Dashboard 来说，虽然一站式可观测是一个趋势，但这并不意味着需要在 Dashboard 上管理所有服务的集群。</p><p>因此，“产品化” 与 “全托管” 将会是 EventMesh Dashboard 锦上添花的能力。“方便运维人员能够在 Web 端为 EventMesh Runtime 集群选取一个 MQ 集群用作 Store，而不是填写一台现有的 MQ 的接入配置”，并不会成为技术选型时的决定性因素。</p><hr><h2 id="2024-亚马逊云科技中国峰会"><a href="#2024-亚马逊云科技中国峰会" class="headerlink" title="2024 亚马逊云科技中国峰会"></a>2024 亚马逊云科技中国峰会</h2><p>正文到此就结束了。以下是我参加本次峰会的一些感想。</p><h3 id="受众"><a href="#受众" class="headerlink" title="受众"></a>受众</h3><ul><li>产品经理、架构师或中小公司 CTO 在议题中主讲的比例相当高，占到所有议题的 60% 以上。</li><li>其中约有 30% 的议题 / 展台与其它公司的解决方案具有一定重合，但在宣传自己的解决方案时并未体现出竞争力。</li><li>Presentation 面向的客户会更偏向于甲方的经理。</li></ul><p><img src="https://static.pil0txia.com/picgo/IMG_20240530_135106.webp" alt="凳子旁可见一只拐杖"></p><p>这位老哥腿脚不太方便，但讲课四平八稳，有点佩服。看得出来，平时坐在电脑前面的时间肯定是很多的，也就能有更多时间在自己的专业领域（韧性系统）内思考。</p><h3 id="展台"><a href="#展台" class="headerlink" title="展台"></a>展台</h3><ul><li>展会与议题是同时进行的。为了逛这个展，我不得不连翘两个议题。</li><li>展台是与各个公司沟通的最佳途径。相较于 CommunityOverCode Asia，本峰会的议题结束后几乎没有时间与讲师对话，但你可以在展台边轻松地与类似的角色聊上一个小时。</li><li>约 50% 的展台配备的人员只有销售，还不是技术型销售。一问三不知，但是打哈哈能力一流。</li></ul><p><img src="https://static.pil0txia.com/picgo/IMG_20240529_133705.webp" alt="乐高城市的可观测性"></p><p><img src="https://static.pil0txia.com/picgo/IMG_20240529_133724.webp" alt="场景说明"></p><p>像是炫 idea 的产物，技术实现上不难。</p><hr><p>AWS 在场地内有一个在各展台答题集章、兑换限量奖品的活动，这种周边往往都是最精致的。然而展台的 Logo 往往都是产品名，集章卡片上只写了公司名，让我化身为了人形 DFS。</p><p>从各展台领到的奖品装满了德勤的镭射袋，其中我最喜欢的是 RedHat 的胸针。可惜，集齐印章后的 “终极大奖” 只是一个看上去很廉价的风扇，恐怕印着 AWS Logo 的包装都更有纪念意义。</p><h3 id="图集"><a href="#图集" class="headerlink" title="图集"></a>图集</h3><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/picgo/IMG_20240529_090402.webp","alt":"世博中心"},{"url":"https://static.pil0txia.com/picgo/IMG_20240529_090506.webp","alt":"峰会 Logo"},{"url":"https://static.pil0txia.com/picgo/IMG_20240529_091050.webp","alt":"大厅内景布置"},{"url":"https://static.pil0txia.com/picgo/IMG_20240529_091619_mosaic.webp","alt":"讲师的挂绳颜色为蓝色，AWS 员工的为橙色"},{"url":"https://static.pil0txia.com/picgo/IMG_20240529_091956.webp","alt":"Keynote 会场"},{"url":"https://static.pil0txia.com/picgo/image-20240612163154351.webp","alt":"EventBridge at AWS re:Invent 2023 SVS401"}]</div>      <div class="gallery-items">      </div>    </div><p><img src="https://static.pil0txia.com/picgo/MYXJ_20240530164448716_fast.webp" alt="最后一场议题结束 | AWS 员工同款周边 T 恤奖品，图案是一朵云"></p><blockquote><p>有趣的是，会后我应邀去小红书总部参观时，被误以为是供应商😄</p></blockquote><p>期待明年再会。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文由 &lt;a href=&quot;https://mp.weixin.qq.com/s/67od28RReSLKDBeDpQOzdg&quot;&gt;Apache EventMesh&lt;/a&gt; 公众号转载。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;AWS-Ev</summary>
      
    
    
    
    <category term="架构" scheme="https://www.pil0txia.com/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="亚马逊云科技中国峰会" scheme="https://www.pil0txia.com/tags/%E4%BA%9A%E9%A9%AC%E9%80%8A%E4%BA%91%E7%A7%91%E6%8A%80%E4%B8%AD%E5%9B%BD%E5%B3%B0%E4%BC%9A/"/>
    
    <category term="AWS" scheme="https://www.pil0txia.com/tags/AWS/"/>
    
    <category term="EventBridge" scheme="https://www.pil0txia.com/tags/EventBridge/"/>
    
    <category term="Dashboard" scheme="https://www.pil0txia.com/tags/Dashboard/"/>
    
    <category term="产品" scheme="https://www.pil0txia.com/tags/%E4%BA%A7%E5%93%81/"/>
    
  </entry>
  
  <entry>
    <title>《铃芽之旅》4K SDR madVR 渲染壁纸</title>
    <link href="https://www.pil0txia.com/post/2024-02-29_suzume-no-tojimari-wallpaper/"/>
    <id>https://www.pil0txia.com/post/2024-02-29_suzume-no-tojimari-wallpaper/</id>
    <published>2024-02-29T11:05:07.000Z</published>
    <updated>2024-02-29T11:05:07.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>我的《铃芽之旅》影评：<a href="https://www.pil0txia.com/post/2023-04-05_suzume-no-tojimari-review/">《铃芽之旅》是新海诚对粉丝的背叛吗？不，是他对自己的忠实。</a></p></blockquote><p>三刷《铃芽之旅》（『 すずめの戸締まり』， Suzume no Tojimari），继续继承前两部作品的审美要求，挑选、截取了 72 张 4K SDR 壁纸。</p><p>可以明显地察觉到影片的分镜资源在向人物的半身景别倾斜，叙事时更多地专注于人物的表情，而非 B-roll 式的场景描写。背景插图中也有少部分采用了 3D 建模。</p><p>这从电影工业的角度来说，对院线新观众是好事；但就动漫这一层面来讲，能让老粉们心血来潮的美景变少了。</p><p>常世中末日火海的景象虽然宏大，但用作壁纸还是过于压抑，因此涉及到的较少。</p><p>特地选用了 SDR 色域，而非 HDR，力求最大程度地还原影院中的色彩表现。</p><p>以下图片使用了 <code>Webp</code> 压缩，经过参数调优，网页浏览的观感没有区别，只有在 200% 放大后才能看出细节的损失。</p><p>你可以从 <a href="https://www.alipan.com/s/q4UpxLJm2js">阿里云盘</a> (提取码: <code>cb76</code>) 或 <a href="https://terabox.com/s/1txyDlf3lb1ztvgM2MKgjzg">TeraBox</a> (提取码: <code>29zc</code>) 打包下载未压缩原图。</p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000131.921.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000134.093.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000136.637.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000151.901.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000154.030.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000245.904.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000257.552.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000303.178.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000601.860.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000602.571.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_000747.099.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001001.171.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001001.934.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001009.692.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001138.155.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001238.630.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001243.762.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001244.012.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001830.730.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001932.085.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_001944.304.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_002314.684.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_002324.407.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_002524.731.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_002846.349.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_003022.611.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_003023.029.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_003024.028.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_003648.705.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_003958.061.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_004002.172.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_004635.417.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_004751.076.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_004852.053.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005044.453.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005047.457.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005057.638.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005058.930.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005137.136.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005203.454.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005536.627.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005643.649.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005655.161.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_005909.837.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_010642.915.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_010853.879.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_011007.912.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_012603.448.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_012707.012.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_013350.955.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_013353.126.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_013619.898.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_013651.963.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_013723.647.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014025.644.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014027.437.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014033.860.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014253.876.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014343.466.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014439.189.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014455.163.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014508.593.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014513.558.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014745.206.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014846.145.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_014933.525.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015244.833.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015247.924.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015259.262.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015307.546.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015605.707.webp"></p><p><img src="https://vc.static.pil0txia.com/gallery/suzume_no_tojimari/Suzume.2022.2160p.BluRay.x265.10bit.DTS-CTRLHD.mkv_015612.157.webp"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;我的《铃芽之旅》影评：&lt;a href=&quot;https://www.pil0txia.com/post/2023-04-05_suzume-no-tojimari-review/&quot;&gt;《铃芽之旅》是新海诚对粉丝的背叛吗？不，是他对自己的忠实。&lt;/a&gt;&lt;/</summary>
      
    
    
    
    <category term="ACG" scheme="https://www.pil0txia.com/categories/ACG/"/>
    
    
    <category term="壁纸" scheme="https://www.pil0txia.com/tags/%E5%A3%81%E7%BA%B8/"/>
    
    <category term="新海诚" scheme="https://www.pil0txia.com/tags/%E6%96%B0%E6%B5%B7%E8%AF%9A/"/>
    
    <category term="4K" scheme="https://www.pil0txia.com/tags/4K/"/>
    
    <category term="Potplayer" scheme="https://www.pil0txia.com/tags/Potplayer/"/>
    
    <category term="madVR" scheme="https://www.pil0txia.com/tags/madVR/"/>
    
    <category term="铃芽之旅" scheme="https://www.pil0txia.com/tags/%E9%93%83%E8%8A%BD%E4%B9%8B%E6%97%85/"/>
    
    <category term="SDR" scheme="https://www.pil0txia.com/tags/SDR/"/>
    
  </entry>
  
  <entry>
    <title>成为 Apache Committer 只是我投身开源社区建设的开始</title>
    <link href="https://www.pil0txia.com/post/2024-01-21_what-is-an-apache-committer/"/>
    <id>https://www.pil0txia.com/post/2024-01-21_what-is-an-apache-committer/</id>
    <published>2024-01-20T18:59:49.000Z</published>
    <updated>2024-01-24T12:55:26.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文已由 <a href="https://mp.weixin.qq.com/s/tAXrI2ckaDXgBmDNONfnUA">Apache EventMesh</a> 公众号转载、<a href="https://mp.weixin.qq.com/s/lCvPgTRvCPaKL6JohaE_kQ">ALC Shenzhen</a> 转发。</p></blockquote><h2 id="新晋-Apache-Committer"><a href="#新晋-Apache-Committer" class="headerlink" title="新晋 Apache Committer"></a>新晋 Apache Committer</h2><p>感谢 Apache EventMesh PMC 邀请我成为 Apache EventMesh Committer。Apache 软件基金会建立于 1999 年，25 年来在全球范围内共计推选了 <a href="https://people.apache.org/committer-index.html">9199 名 Committer</a>。很荣幸能成为全球第 9200 名 Apache Committer，如愿拥有了属于自己的 <a href="mailto:xiatian@apache.org">Apache 邮箱</a>。</p><p><img src="https://static.pil0txia.com/picgo/image-20240120020652945.webp" alt="Apache ID"></p><p><img src="https://static.pil0txia.com/picgo/image-20240122161847105.webp" alt="Apache Committers 在全世界的分布"></p><h2 id="Apache-开源社区的魅力"><a href="#Apache-开源社区的魅力" class="headerlink" title="Apache 开源社区的魅力"></a>Apache 开源社区的魅力</h2><h3 id="契机"><a href="#契机" class="headerlink" title="契机"></a>契机</h3><p>动笔写下这个章节的第一个字时，是我在 GitHub 上泡了一整天之后。<a href="https://github.com/apache/eventmesh/issues/888">Good First Issues</a> 和 <a href="https://github.com/apache/eventmesh/issues/3873">What are we doing now</a> 这两个重要的 List 已经近半年没有人维护了，我根据自己还只是 Contributor 时整理的笔记，将零散的代码改进点创建为新的 good first issue，供想要参与开源的新人练手。</p><p>令我意外的是，就在我建了两个低难度的 issue 之后，十分钟不到就已经双双有人认领。有新面孔来做 good first issue 的时候仿佛已经过去很久了。Good First Issue List 中的任务大多都早已有人认领，但提交的 PR 在被 Review 之后就杳无音信，又或是没有提交过 PR。</p><p>原来关注着社区的人并没有减少，只是留给他们的<strong>契机</strong>变少了。回想到我自己，谁都需要这样的一个契机。</p><h3 id="能力越大-责任越大"><a href="#能力越大-责任越大" class="headerlink" title="能力越大 责任越大"></a>能力越大 责任越大</h3><p>配置好 Committer 权限的第一天，我就在<a href="https://lists.apache.org/list?issues@eventmesh.apache.org:2024-1">邮件列表</a>里创建了 50 条回复。我跟进了几乎所有长时间没有进展的 PR 和 issue，在具有 Committer 权限后的半个月里，邮件列表中发送了超过 200 封邮件。</p><p>成为 Committer 之后，我更加确信：在开源社区，<strong>Committer 身份的意义</strong>就是 “能力越大、责任越大”。我可以不用麻烦别人过来关闭一个过时的 issue 或是打上一个 <code>help wanted</code> Label，而是可以出于个人的主张自行维护；我的 Approve 不再是灰色的 “意见”，而是绿色的 “肯定”。推选为 Committer 是社区对你的一种认可，这意味着你可以<strong>独当一面</strong>，而不用事事询问 PMC。</p><p><img src="https://static.pil0txia.com/picgo/image-20240121004726444.webp" alt="没有仓库写权限的 Reviewer 头像右侧的 ✔ 是灰色的"></p><p>我意识到我是突然这么活跃的。大家在开源社区里能投入的时间都是有限的，没有人能够长此以往地全身心地为社区 Review 每一个贡献。但开源社区就是这样<strong>运作</strong>的 —— 没有谁需要牺牲什么来去达成什么，也没有谁无可替代地具有这个 PR 绝对的话语权。</p><p>我看到了，我有话要说，所以我 Review。这是最自在的 Reviewer 动机。</p><h3 id="这群人"><a href="#这群人" class="headerlink" title="这群人"></a>这群人</h3><p>GitHub 的 Issue 和 Pull Request 很强大，但它只是提供了一个问题追踪的平台而已。若是想要细数一个开源社区的里程碑，像是亲历者一样历历走来，这时你就记得邮件列表的好了。</p><p>相比于一天动辄十几封邮件的 <a href="https://lists.apache.org/list.html?dev@eventmesh.apache.org">dev</a> 和 <a href="https://lists.apache.org/list.html?issues@eventmesh.apache.org">issues</a> 列表，<a href="https://lists.apache.org/list?users@eventmesh.apache.org">users</a> 邮件列表要来得清净的多。这是 EventMesh 最早从项目进入孵化器之初就开始活动的邮件列表，用于发布项目的里程碑、解答用户的疑问，也是能够看到如今在工作和开源两端忙碌着的明哥和陈老板在两年前是怎样的一个青涩模样的地方。</p><p>PMC Chair 陈广胜发起了一个<a href="https://lists.apache.org/thread/2hblm7dpvlstf65dg44g0fr0kp1pq6lb">针对 EventMesh Logo 的讨论</a>，初始成员们在下面有序地发表着自己的意见，邮件的附件中还带着他们中意的 Logo 截图，而项目的孵化器导师 Justin 则在下面有点气又很礼貌地提醒不要跨越多个邮件列表发表回复。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/picgo/image-20240120215620098.webp","alt":"第一次提醒"},{"url":"https://static.pil0txia.com/picgo/image-20240120222348491.webp","alt":"第二次吐槽"}]</div>      <div class="gallery-items">      </div>    </div><p>看到这里，我回想起来，在<a href="../2023-08-28_community-over-code-asia-2023/#%E8%AE%B2%E5%B8%88%E5%AE%B4">我和 Justin 合影</a>前介绍自己时，我说，我是来自 EventMesh 社区的贡献者。他说，“哦！我知道这个项目。”</p><p>那时的我没有多想，但现在的我突然意识到，Justin 的眼神里有一份肯定，就像是把孵化毕业的项目当作了自己的孩子一样。EventMesh 历时两年多的孵化，23 年 2 月毕业成为 TLP 顶级项目，几个月后就能有社区培养的贡献者与孵化器导师线下见面，在 Justin 指导的项目中应该也是少有的吧。</p><p>这篇讨论令我有些忍俊不禁，如果这群人这样的讨论发生在现在，我想我得在会上笑出声来。</p><p>这就是开源社区的魅力之一。相比于 “开放源代码” 这字面意义上的含义，有着<strong>一起玩开源的这群人</strong>才更难能可贵。<strong>发现</strong>并<strong>培养</strong>喜欢开源的成员，也正是 PMC Member 和 Committer 们的职责。</p><h3 id="Committer-≠-Reviewer"><a href="#Committer-≠-Reviewer" class="headerlink" title="Committer ≠ Reviewer"></a>Committer ≠ Reviewer</h3><p>不得不感慨的是，<strong>新兴</strong>的 EventMesh 社区中被提名为 Committer 后持续活跃超过半年的 <strong>Reviewer</strong>，与 ASF 中的热门项目（例如 <a href="https://lists.apache.org/list.html?users@kafka.apache.org">Kafka</a>）相比，还是有些少。从项目进入孵化器开始，每隔几个月都能在邮件列表中见到<a href="https://lists.apache.org/list?users@eventmesh.apache.org:2023-1">新 Committer 公告</a>，都是我在几年前的 issue 里见到过很多次的身影。他们曾经做出了很多贡献，如今却完全见不到他们来提 issue、Review PR 了。</p><p>23 年 8 月到 10 月的这段时间，我有<a href="../2023-07-06_stream-comparator-and-lambda-sort/">实习</a>，还要进行秋招，但<a href="https://github.com/Pil0tXia">我在开源社区</a>依然稳定地每周提交 1~3 个 PR，同时 Review 其他人的 PR。在我心中，开源的<strong>优先级</strong>更高，因为我知道，这是更快提升个人技术实力的方法。也正是因为我在开源社区的积累，帮助我拿到了大厂的 ssp offer。</p><p>但在接下来的一个月里，我忙于选 offer，花时间调研业务的前景，暂时停下了开源的脚步，因为选择往往比努力更重要。此时一些能够决定我的命运的事情的优先级被置于开源之上，那么对于其他人而言，也一定时常有令其不得不放下开源的事情。毕竟，玩开源只是一个非常有意思、也很有意义的<strong>兴趣</strong>。</p><p><img src="https://static.pil0txia.com/picgo/image-20240121015753096.webp" alt="不知何时起 Code review 一维已经独占鳌头"></p><p>无论如何，他们都是 Apache 的荣誉 Committer，毕竟每个人都有自己的事情。同时满足 “不受雇于项目的母公司”、“出于个人兴趣参与开源”、“能花时间 Review PR”、“能参与项目发展的讨论” 这些条件的 Committer 还是少数，而这些 Committer 才是很多社区最为<strong>刚需</strong>的成员。</p><h2 id="学生如何通过开源之夏成为-Apache-Committer"><a href="#学生如何通过开源之夏成为-Apache-Committer" class="headerlink" title="学生如何通过开源之夏成为 Apache Committer"></a>学生如何通过开源之夏成为 Apache Committer</h2><p>开源的初心是很纯粹的，Apache Software Foundation 的存在只是为了在法律意义上保护项目和开发者们。</p><p>开源之夏只是你接触开源的众多理由中的其中之一，它是比较适合在校生参与开源活动的一个起点。</p><p>有很多同学问过我开源或是开源之夏的窍门，希望这个章节能对你有所帮助。</p><h3 id="抓住开源之夏这个契机"><a href="#抓住开源之夏这个契机" class="headerlink" title="抓住开源之夏这个契机"></a>抓住开源之夏这个契机</h3><p><strong>你可以带着功利心去接触开源，但这并不妨碍你从中获得乐趣。</strong>为了简历好看方便校招也好，单纯参加比赛顺便领个奖金也罢；想要从同龄人中脱颖而出，又或是不满于工作中所写的冗长的业务代码 —— 与其漫无目的地半途而废，不如怀揣着一份荣誉或愿望。真正参与到社区中后，你会自然而然地把 Committer 和 Apache 邮箱当作一种能力的象征和自己的里程碑。</p><p><strong>相比于走上开源之路却没能坚持下去，更多的人缺少的是一个契机。</strong>开源之夏就是很好的一个契机，谷歌、中科院、CCF 和一些大厂都有在办。一个课题、一位导师、一位学生，资源分配合理，哪怕导师在工作上再忙，也能分出精力去指导学生。只要学生愿意虚心请教，就不太容易出现闷声撞墙的情况。导师也希望你能够顺利完成课题，因为这不仅关乎着社区的声誉，导师也想要对你申请课题时的认真态度作出回应。</p><p><strong>Apache 的开源社区欢迎新面孔。</strong>相比于与其他候选人比较而踯躅，不如多花一些时间参与到社区的交流过程中。做一两个 good first issue 或者参加双周例会都是很好的起点。就算没能中选，在自己的代码被 Review 的过程中也能稳步地提升自己的编码实力。</p><h3 id="专注于开源，而不是开源之夏这个活动"><a href="#专注于开源，而不是开源之夏这个活动" class="headerlink" title="专注于开源，而不是开源之夏这个活动"></a>专注于开源，而不是开源之夏这个活动</h3><p><strong>课题没有中选，不代表你的开源之夏结束了。</strong>能拿出来作为一个课题的特性只占社区想做的特性中的一小部分，通常还是比较简单的那一批。参与开源没有门槛，无论课题的申请截止与否，也无论学生是否是课题的候选人，只要你阅读过贡献指南，按照格式提交 PR，说明自己修改的内容和目的，社区都会同样重视。你甚至可以与中选者合作，毕竟有的课题是需要持续迭代下去的，需要周边生态的配合。如果有人，不是为了比赛，依然能出于兴趣去做这件事，我觉得会更受人关注。</p><p><strong>因为开源之夏，你已经认识到了去做开源的这件事情。</strong>如果你不是那种需要 deadline 推着走的 type 的话，按照你的节奏去加入社区就可以了。大多数参加开源之夏或者 XX 比赛的同学都只是将课题视为一个任务，没有持续地做下去。事实证明能不能获得 Committer 提名也与比赛有没有获奖根本没有关系。贡献的多少是由社区的大家看在眼里的，只要你能在一个社区有持续性地贡献，都能被认可，并成为相应模块的专家。</p><p><strong>不要只是抱着 “学习” 的心态参与开源社区。</strong>一个 Apache 项目是你不可能在短时间内看完并理解的，你也不需要 “学完” 它才能上手。经验是在实际优化代码、阅读 git commit 对应的 PR 的过程中才能积累的，没有机会会乖乖等到你准备好了再让你抓住。哔哩哔哩上通常会有项目的 Meetup，可以让你很快理解重点。</p><h2 id="纪念"><a href="#纪念" class="headerlink" title="纪念"></a>纪念</h2><p>写下此文，更多的是为了<strong>自勉</strong>。疲惫了也好，稍作休息也罢，读一读过去的自己，以此明志，总能重拾热情。感谢在我朝着目标努力的路上注视着我的各位。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/picgo/image-20240120012304258.webp","alt":"邮件邀请"},{"url":"https://static.pil0txia.com/picgo/20240108232705.webp","alt":"花名册"},{"url":"https://static.pil0txia.com/picgo/20240112014941.webp","alt":"Committer Map China"},{"url":"https://static.pil0txia.com/picgo/image-20240120024034571.webp","alt":"FOAF 1"},{"url":"https://static.pil0txia.com/picgo/image-20240120024126322.webp","alt":"FOAF 2"},{"url":"https://static.pil0txia.com/picgo/20240108191115.webp","alt":"GitHub Invitation"},{"url":"https://static.pil0txia.com/picgo/20240111211637.webp","alt":"JetBrains 全家桶"}]</div>      <div class="gallery-items">      </div>    </div>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文已由 &lt;a href=&quot;https://mp.weixin.qq.com/s/tAXrI2ckaDXgBmDNONfnUA&quot;&gt;Apache EventMesh&lt;/a&gt; 公众号转载、&lt;a href=&quot;https://mp.weixin.qq.co</summary>
      
    
    
    
    <category term="生活" scheme="https://www.pil0txia.com/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="Committer" scheme="https://www.pil0txia.com/tags/Committer/"/>
    
    <category term="ASF" scheme="https://www.pil0txia.com/tags/ASF/"/>
    
    <category term="社区" scheme="https://www.pil0txia.com/tags/%E7%A4%BE%E5%8C%BA/"/>
    
    <category term="开源之夏" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90%E4%B9%8B%E5%A4%8F/"/>
    
  </entry>
  
  <entry>
    <title>Apache 社区力量的汇聚 &amp; 程序员奔现大会：CommunityOverCode Asia 2023 给我的成长</title>
    <link href="https://www.pil0txia.com/post/2023-08-28_community-over-code-asia-2023/"/>
    <id>https://www.pil0txia.com/post/2023-08-28_community-over-code-asia-2023/</id>
    <published>2023-08-28T08:56:30.000Z</published>
    <updated>2023-08-28T15:20:30.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>本文已由 Apache EventMesh 公众号转载，<a href="https://mp.weixin.qq.com/s/gceJaIoosT68aMEv1vPC6g">ALC Beijing</a> 和 <a href="https://mp.weixin.qq.com/s/TE6WyS_b7c0qWB7JA2xN3A">ALC Shenzhen</a> 转发：<a href="https://mp.weixin.qq.com/s/fbpgSd1Ex5v-cRI5I1vqCQ">我与 EventMesh 共赴盛会：CommunityOverCode Asia 2023 回顾</a></p></blockquote><h2 id="初识"><a href="#初识" class="headerlink" title="初识"></a>初识</h2><p>今年的 Apache 软件基金会亚洲峰会改名了。在参加会议前，我还是愿意用 “<a href="https://apachecon.com/acasia2023/">ApacheCon Asia 2023</a>” 来称呼它，似乎只有这样才能让人知道，你参加的是 Apache 的会议。在提笔写下这段感想时，我突然想通了，“<a href="https://www.bagevent.com/event/cocasia-2023">CommunityOverCode Asia 2023</a>” 的这个名字，已经深入我的内心。</p><p>无所谓 Apache 字眼存在与否，因为只有 Apache 才能做出这样的 Community。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_091335.webp" alt="见到 Apache Logo 后拍的第一张照片"></p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/gallery/apache/IMG_20230818_091504.webp","alt":"IMG_20230818_091504"},{"url":"https://static.pil0txia.com/gallery/apache/3X5A6834-opq553929072.webp","alt":"3X5A6834-opq553929072"},{"url":"https://static.pil0txia.com/gallery/apache/IMG_20230818_095215.webp","alt":"IMG_20230818_095215"},{"url":"https://static.pil0txia.com/gallery/apache/IMG_20230818_095223.webp","alt":"IMG_20230818_095223"}]</div>      <div class="gallery-items">      </div>    </div><h2 id="讲师宴"><a href="#讲师宴" class="headerlink" title="讲师宴"></a>讲师宴</h2><p>要说我收获最大的是什么，那必然是 18 号第一天晚上的这场讲师宴了。议题的干货也很重要，但绝对比不上这场晚宴带给我的自信和决心。</p><hr><p>第一天上午，我听着 Keynote 会场，低着头回味各个赞助商发放的特色小礼品，整理被塞得满满当当的背包和帆布袋。</p><p>唯一空手而归的展位是微众开源的，在那里我一眼认出了桌上最后一件印着 EventMesh 的 T 恤，也被贺繁一眼认出是 EventMesh 的开发者。可惜这一件似乎仅供展示，M 码的我也穿不上。Major_He 很热情，说会寄给我一件，还将我称为核心开发者介绍给了身边的同事。于是我内心狂喜表面镇静地将自己的邮寄地址发给了贺繁。</p><p>抬起头看大屏幕上的同声传译，机翻质量有些糟糕，突然就听不懂了，遂还是低下头去。那时的我绝对想不到，晚上能与贺繁和陈老板跑去长安街逛一遭。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_094700.webp" alt="背包装不下了，领个袋子继续装"></p><hr><p>第一天中午，还没换上讲师 T 恤时，我坐着想了五分钟，下定决心准备好”May you be so kind as to join a photograph with me?” 的台词，去找 Justin 合了影，应该是全场最速吧。</p><p>那时的我绝对想不到，晚上的我会因为英语口语最好，半自告奋勇半被怂恿地上台，约 Craig 和 Rich 与 EventMesh 社区的大家合影。等到第三天的时候，我已经可以轻松自在地将 EventMesh 的 L 码的 T 恤赠予 Justin 了（因为 M 码的穿不下）。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_123721.webp" alt="ASF 董事 Justin Mclean，意外的热情，背后是 Craig"></p><hr><p>第一天下午，和我的 GLCC 课题导师薛炜明坐在一起听 Pulsar。我拘谨礼貌地认真听讲，却发现暂时没有什么技术点，“沉稳” 地对着酒店的白纸与明哥写写画画，从我对 EventMesh 的内部需求，到双双掏出笔记本电脑解决仓库 DockerHub 密钥的加密。</p><p>明哥在他小小的 MBP 屏幕上打开了数十个标签页，每个标签页都被挤得只剩个 favicon。我吐槽了一句，“这标签页也太多了”。</p><p>他欣然一笑，“是吧？” 然后切到多任务，三个满满当当的浏览器，“我还有！（笑）”</p><p>那时的我绝对想不到，到晚上吃饭时，能被明哥<del>调侃</del>称赞精力充沛：“（对陈老板）你看他的提交时间就知道了 ——（转头向我）每次我要睡觉了，你的（commit）邮件就叮叮咚咚地发过来了”。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_185017.webp" alt="晚宴开始前的我还不知道接下来能与大家相谈甚欢"></p><p>也正是从这里开始，我的话题有了导向：作为全场最年轻的与会者，我没有一桌 Leader 开口动辄 “我十几年前是做什么业务” 的经验，但我有联想实习 965 的时间，和愈来愈盛的对 EventMesh 的满腔热爱。</p><p>当我被问到 “如果你能在联想实习转正，你会留下来吗？” 的时候，我不假思索地回答道：</p><p>“我会！因为这样我才有时间搞开源”</p><p>大家笑得很开心，然后仿佛每个人都思考了一下自己。</p><p>在他们眼里，我的唯一优势，也是最大优势，就是年轻。虽然我看他们也很年轻，眉飞色舞，神采飞扬，是我多年后想成为的样子，但我的导师薛炜明都已经有白发了。</p><p>正如会前我所请教的一位腾讯 PM 所说，“害，年轻人就是这点不好，总是惶恐”。我能和他们坐在同一张餐桌上，就是一种进步。</p><p><img src="https://static.pil0txia.com/gallery/apache/JG2A7195-opq554777944.webp" alt="别人视角里的我，中间桌右数第二个"></p><p>我认识到了许多大佬，除了微众的三位 Apache EventMesh PPMC 之外，还有来自各个互联网公司的 Leader 们，比如帮我确定 Offset Manager 及其对应的集群架构的华为云中间件团队负责人王海军老师，他总是谦虚自己以前是做手机的；</p><p>比如光看 T 恤就向他的 360 数科同事介绍我是 EventMesh 的讲师的 githublaohu，他总是拉着我帮他推进 PR（我会携着你的设计思想走下去的）；</p><p>比如午餐时遇到了字节 Flink 团队的小伙伴，还有每一场议题都倾囊相授的讲师们。</p><p>写这篇博客，多少是有些想要展示这张合照的意愿、记录一次邂逅：</p><p><img src="https://static.pil0txia.com/gallery/apache/mmexport1692365420945.webp" alt="贺繁，夏天（我），陈广胜，Craig，Rich，薛炜明，王海军"></p><p><img src="https://static.pil0txia.com/gallery/apache/mmexport1692365399427.webp" alt="EventMesh 与 RocketMQ 成员，老胡夹在我和明哥中间，最右边是腾讯云的李伟"></p><h2 id="议题"><a href="#议题" class="headerlink" title="议题"></a>议题</h2><h3 id="学"><a href="#学" class="headerlink" title="学"></a>学</h3><p>说回大会的干货上。第一天下午，明哥中途离开之后，我再抬起头来，就已经听不懂了。就是这张 PPT：</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_143947.webp" alt="Apache Pulsar 3.0 的限流方案"></p><p>很尴尬，我到现在还是看不懂。只能等一个月之后放出的录播了。能看懂的小伙伴留个评论好吗，我会给你点个赞的。</p><p><img src="https://static.pil0txia.com/gallery/apache/pil0txia-github-thumb-up.webp" alt="👍"></p><p>但在这之后的议题，我就都能听懂了，绝大部分。</p><p>在此之前，我想出了一大堆诸如 Kafka 准确定位 offset 及 offset 存储位置、微服务无状态水平扩展、Doris 实时同步分布式数据库异构数据以供实时报表分析之类的问题，拷打我的同事。在这之后，我就几乎没有再问了，因为没有空。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230826_151702.webp" alt="整场大会我的笔记与待办"></p><p>这些议题一下子就让我求知若渴起来了，陌生的名词会立刻谷歌，力求以最快的速度理解这个概念，以免影响后续的理解。</p><p>《Deep Dive the replication protocol in Kafka》这一场讲的真的特别好，来自台湾的 Luke Chen 老师用最直白的数组深入浅出地讲解了 ISR (In-Sync Replica) 的种种 Corner Case 及其应对机制，让我感受到了 Red Hat Engineer 的魅力。</p><p>同事听到我这么高的评价，说 “估计年底能看录屏”。我回复，“回头我讲给你听😁”。是的，回到公司后，我就把学到的东西全都<del>捉住并</del>分享给了同事们。</p><p>听的真的很开心，越听越带劲，越听越有精神。哪怕晚上只睡了 6 个小时，挤了一个小时的北京地铁早高峰，来到会场也能轻松立刻进入活跃思考。</p><p>这也带来了我的好心情，茶歇时间吃吃点心，喝杯咖啡和红茶，坐下来接着听。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230818_154958.webp" alt="VIP 集齐印章兑换的讲师 T 恤，质地很舒服"></p><p>当别的老哥是这种状态的时候：</p><p><img src="https://static.pil0txia.com/gallery/apache/DSC_5890-opq557468976.webp" alt="不好意思老哥"></p><p>我是这样的：</p><p><img src="https://static.pil0txia.com/gallery/apache/3X5A9101-opq557460588.webp" alt="照片水平从右往左第三个伸着脖子的是我"></p><p><img src="https://static.pil0txia.com/gallery/apache/3X5A9104-opq557460554.webp" alt="水蓝色 Polo 衫的那个是我，你会发现程序员的穿衣特点"></p><p><img src="https://static.pil0txia.com/gallery/apache/JG2A7631-opq555681391.webp" alt="第一排最右边的是我"></p><p><img src="https://static.pil0txia.com/gallery/apache/3X5A9457-opq557737372.webp" alt="拍照的手举得更高一些的是我"></p><p>别的丑照我就不放了，有人感兴趣的话可以去<a href="https://as.alltuu.com/album/1261715204/">图片直播</a>里找。我很不理解为什么那么多人，非要逮着我拍长焦大头特写。</p><h3 id="用"><a href="#用" class="headerlink" title="用"></a>用</h3><p>19 号下午 RPC / Service Mesh 会场的议题几乎被 Dubbo 占领了，厅又小，一开始都被人塞满了。但是到最后慢慢的就人少了，只剩下几个人。</p><p>我从头听到尾，结束后出品人刘军将唯一一个印着 Dubbo Logo 的电脑包送给了我。（我当时只觉着类皮革的质感很好，没想到回去一搜吊牌可以值回大半票价。）</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230819_141814.webp" alt="已经排到门外了"></p><p>《RocketMQ 百万队列能力支持 – RocksDB kv 存储》这一场让我看到了阿里的研发实力，对一个消息队列的优化都已经深入到数据库了。赵福建讲师非常自信，让我一个不太懂数据库底层原理的 Java 开发都听得津津有味。（虽然中途和坐在身边的王海军轻声嘀咕了一句听不太懂，他也表示同感，被坐在前一排的出品人听见了，对我俩表示了关心🤣然后我就立刻去搜了几个概念，接下来也如鱼得水了）</p><p>《小米 RocketMQ 的降本增效和容灾实践》对多集群节点多活、跨 AZ 数据多活和双机房自动切流的三种消息队列集群架构的优缺点做了很好的分析；《云原生消息流系统 Apache RocketMQ 在腾讯云的大规模生产实践》中讲到用子母时间轮代替定时消息的 timer 时，我对这种巧妙的数据结构喜形于色……</p><p>在熟悉了几个大厂分享的架构实践之后，不用总结优缺点，几个节点和注册中心在我的脑海里组合一下，再编制一下输入用例，便能清楚其预期行为。</p><p>我在听《OpenSergo &amp; Dubbo 微服务治理最佳实践》这一场时，部门领导在微信群里问了一个问题，大概是微服务与 K8s 的充分必要关系。我抄起一张大白纸，画了一张架构图，拍下来发到群里，解答了一连串问题。这张架构图相比场上的分享来说再简单不过，但放在以前的我身上，是没法保证十足的信心的。</p><p>新掌握的知识就能学有所用，我的手微微颤抖。</p><p>回到公司，数开 Leader 与我约了 GSC DW 新架构的可行性讨论。我要在接下来的一周内想通 Apache Doris 2.0 的落地方案，以逐步减轻 SAP HANA 的存储压力。</p><h2 id="激励"><a href="#激励" class="headerlink" title="激励"></a>激励</h2><p>不知道为什么，从大会结束的这一天起，我仿佛变了一个人一样 —— 变得对 Coding 更狂热了，完全适应了这种 “白天写业务，晚上写中间件” 的模式。</p><p>就像调试模式一样，大脑可以立刻打下断点，灵活地在各项业务中切换思考的上下文。一次编码的连续专注时间可以达到 10 个小时以上 —— 这是根据我上周三从下午一点半一直写到晚上 12 点算的。其实中午吃饭的时候我也在想 Kafka 的 Offset 遍历，思维没被打断，但我怕写 14 个小时以上会被人害怕，所以没算上午的时间。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230824_000014.webp" alt="你见过凌晨 12 点整的联想吗？物业没有关灯"></p><p>无暇顾及生活中的其它琐事，甚至肚子也不会饿，凌晨走回家吃上一罐八宝粥就算作晚饭了。我很喜欢这种如若无人之境的状态，它能给我一天充实的利用感。</p><p>这个周末也是，除了打磨秋招的简历之外，还要写这篇博客，总结笔记，写千字五百元的 AWS 约稿，实现 Kafka 的管理 API，RabbitMQ 的 BugFix 合并之后提交其余 storage-plugin 的 admin 实现，开会讨论 EventMesh admin 模块的功能和选型，关注 site 仓库三个还没合的 PR 及其合并之后在此基础上的完善…… 这篇博客在我恍惚之间写到了早上五点半，等我再次抬起头时已经天亮了。很有盼头。</p><h2 id="周边"><a href="#周边" class="headerlink" title="周边"></a>周边</h2><p>25 号周五，我就收到了来自微众和思否的包裹：</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230824_190044.webp" alt="Apache Logo 的搪瓷杯准备放在桌上当吉祥物"></p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230824_192649.webp" alt="鼠标垫，十分满足。虽然锁边略草率，但在大会 Logo 上 Coding 真的很酷"></p><p>其中最期待的就是 EventMesh 的贴纸，我也要把自己的电脑贴成腾讯云的李伟老师这样：</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230820_173932.webp" alt="甚至还有一张 ApacheCon Asia 2022 的贴纸，技高一筹了"></p><p>我整场大会最后一位交谈的讲师就是李伟老师，看到负责 RocketMQ 的他贴了 EventMesh 的贴纸，便问了 EventMesh 与 RocketMQ EventBridge 的区别。前者更接近 Service Mesh 的思想。</p><p><img src="https://static.pil0txia.com/gallery/apache/IMG_20230825_154547.webp" alt="又现代又复古，很整活"></p><p>每每看到这张鼠标垫，都不会忘记自己邂逅 CommunityOverCode Asia 的新鲜感。</p><p>穿 Apache T 恤，戴 Apache ShardingSphere 勋章，背 Apache Dubbo 双肩包，贴 Apache EventMesh 贴纸，用 Apache Software Foundation 水杯，垫 CommunityOverCode Asia 桌垫，玩 Apache Doris 飞盘，彻底变成 Apache 人。</p><h2 id="ALC-Shenzhen"><a href="#ALC-Shenzhen" class="headerlink" title="ALC Shenzhen"></a>ALC Shenzhen</h2><p>能够成为 ALC Shenzhen 社区成员之一，也是令我受宠若惊的。ApacheCon Asia 2021 2022 和 CommunityOverCode Asia 2023 就是由 ALC Beijing 筹办的。2024 年，CommunityOverCode Asia 将来到深圳或杭州，吸引更多本地的开发者。</p><h2 id="鸣谢"><a href="#鸣谢" class="headerlink" title="鸣谢"></a>鸣谢</h2><p>真的很感谢陈广胜老师送给我的社区内部 VIP 票，带给了我与大家共进晚餐的机会（以及穿上这件帅气的讲师 T 恤）。票很少，候选者很多，微众开源作为大会的白金赞助商之一，将 SPONSOR 票种分散给了更多开源的火种们。我所中选的 GLCC 课题也是由微众开源赞助的，因此无需在 GitLink 平台提交中期考核 PR，而是得以在 GitHub 更好的维护社区。</p><p>感谢贺繁那么有心，一回深圳就用顺丰寄来了我一眼相中的、印着 EventMesh Logo 的 T 恤。</p><p>感谢姜宁老师为 ALC Beijing 筹办大会付出的种种努力。看过您的董事连任致辞，ApacheCon 从 2021、2022 年的线上走到 2023 年的线下很不容易，也需要更多的厂商赞助支持。回到天津后才意识到忘记领取大会礼包，深夜给您写了邮件，您二话不说就让思否给我寄了过来。</p><p>感谢薛炜明导师的悉心教授，是您给了我一点小小的开源震撼，在我心里种下了一颗小小的种子。我才能那么快的融入社区，接手种种事宜。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="主会场合影"><a href="#主会场合影" class="headerlink" title="主会场合影"></a>主会场合影</h3><p>可以看到我站在最后一排高举着 EventMesh T 恤：</p><p><img src="https://static.pil0txia.com/gallery/apache/3X5A8970.webp" alt="可以看不见我，但不能看不见 EventMesh"></p><p><img src="https://static.pil0txia.com/gallery/apache/image-20230828025451935.webp" alt="可惜合照的体积只有 1 兆，还是看不见"></p><p><img src="https://static.pil0txia.com/gallery/apache/3X5A8985.webp" alt="只有赞助商才有牌子"></p><p><img src="https://static.pil0txia.com/gallery/apache/image-20230828025759193.webp" alt="这下看见了，微众的海拔最高"></p><h3 id="分会场合影"><a href="#分会场合影" class="headerlink" title="分会场合影"></a>分会场合影</h3><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/gallery/apache/3X5A7740-opq554439766.webp","alt":"3X5A7740-opq554439766"},{"url":"https://static.pil0txia.com/gallery/apache/3X5A7836-opq554572937.webp","alt":"3X5A7836-opq554572937"},{"url":"https://static.pil0txia.com/gallery/apache/3X5A9518-opq557897579.webp","alt":"3X5A9518-opq557897579"}]</div>      <div class="gallery-items">      </div>    </div><p><img src="https://static.pil0txia.com/gallery/apache/3X5A7771-opq554515790.webp" alt="这是明哥走之后的下一场，最右边的是我"></p><h3 id="会场环境"><a href="#会场环境" class="headerlink" title="会场环境"></a>会场环境</h3><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://static.pil0txia.com/gallery/apache/DSC_2787.webp","alt":"DSC_2787"},{"url":"https://static.pil0txia.com/gallery/apache/DSC_2818.webp","alt":"DSC_2818"},{"url":"https://static.pil0txia.com/gallery/apache/DSC_2836.webp","alt":"DSC_2836"},{"url":"https://static.pil0txia.com/gallery/apache/3X5A6998-opq554006010.webp","alt":"3X5A6998-opq554006010"},{"url":"https://static.pil0txia.com/gallery/apache/JG2A7004-opq554754965.webp","alt":"JG2A7004-opq554754965"},{"url":"https://static.pil0txia.com/gallery/apache/JG2A7057-opq554752353.webp","alt":"JG2A7057-opq554752353"}]</div>      <div class="gallery-items">      </div>    </div><p>感谢阅读到这里的你，愿意聆听我的故事。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;本文已由 Apache EventMesh 公众号转载，&lt;a href=&quot;https://mp.weixin.qq.com/s/gceJaIoosT68aMEv1vPC6g&quot;&gt;ALC Beijing&lt;/a&gt; 和 &lt;a href=&quot;https://m</summary>
      
    
    
    
    <category term="生活" scheme="https://www.pil0txia.com/categories/%E7%94%9F%E6%B4%BB/"/>
    
    
    <category term="实习" scheme="https://www.pil0txia.com/tags/%E5%AE%9E%E4%B9%A0/"/>
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="CommunityOverCode" scheme="https://www.pil0txia.com/tags/CommunityOverCode/"/>
    
  </entry>
  
  <entry>
    <title>【开源贡献笔记】异步文件事件监听：协调稳健的 Webhook 配置缓存同步</title>
    <link href="https://www.pil0txia.com/post/2023-08-11_sync-cache-under-async-events/"/>
    <id>https://www.pil0txia.com/post/2023-08-11_sync-cache-under-async-events/</id>
    <published>2023-08-11T08:54:43.000Z</published>
    <updated>2023-08-23T12:29:43.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="在异步的文件系统事件中同步加锁写入"><a href="#在异步的文件系统事件中同步加锁写入" class="headerlink" title="在异步的文件系统事件中同步加锁写入"></a>在异步的文件系统事件中同步加锁写入</h2><p>PR 链接：<a href="https://github.com/apache/eventmesh/pull/4344">https://github.com/apache/eventmesh/pull/4344</a></p><h3 id="功能说明"><a href="#功能说明" class="headerlink" title="功能说明"></a>功能说明</h3><p>当创建一个新的 <code>WebHookConfig</code> 时，会调用 <code>writeToFile</code> 方法，创建一个文件并写入数据。<code>fileWatchRegister</code> 可以监测文件的创建、修改和删除事件，并且在创建和修改的时候调用 <code>cacheInit</code> 方法，将 <code>WebHookConfig</code> 写入 <code>cacheWebHookConfig</code>。</p><h3 id="Bug-现象"><a href="#Bug-现象" class="headerlink" title="Bug 现象"></a>Bug 现象</h3><p>正常情况下，复制 <code>webhook.github.eventmesh.all</code> 配置文件时，会产生 ENTRY_CREATE 和 ENTRY_MODIFY 两个事件，这符合预期：</p><p><img src="https://static.pil0txia.com/picgo/image-20230811171529822.webp"></p><p>Bug 出现在首次调用 <code>insertWebHookConfig</code> 端点创建 <code>WebHookConfig</code> 的时候，只能捕捉到 ENTRY_CREATE 事件（不正常，预期的行为应该也是两个事件）：</p><p><img src="https://static.pil0txia.com/picgo/image-20230811171550401.webp"></p><p>此时文件还没有被写入数据，是一个空文件，因此 <code>cacheInit</code> 会向 <code>cacheWebHookConfig</code> 写入 <code>null</code>：</p><p><img src="https://static.pil0txia.com/picgo/image-20230811171612069.webp"></p><p><img src="https://static.pil0txia.com/picgo/image-20230811171617362.webp"></p><p>然而，循环一次就结束了，<code>fileWatchRegister</code> 并没有在文件写入 <code>WebHookConfig</code> 数据后，再次调用 <code>cacheInit</code> 方法。因此，<code>cacheWebHookConfig</code> 中没有有效值，GitHub 的 Webhook 调用将无法检索到对应的 Webhook 配置。</p><p><img src="https://static.pil0txia.com/picgo/image-20230811171633924.webp"></p><h3 id="初步解决方法"><a href="#初步解决方法" class="headerlink" title="初步解决方法"></a>初步解决方法</h3><p>我在捕获到文件事件之后添加了一点延时，解决了这个 bug：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> {</span><br><span class="line">    <span class="keyword">assert</span> service != <span class="literal">null</span>;</span><br><span class="line">    key = service.take();</span><br><span class="line">    Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">} <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">    log.error(<span class="string">"Interrupted"</span>, e);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>添加延时后，就可以捕获到 ENTRY_CREATE 和 ENTRY_MODIFY 两个事件了。<code>for (final WatchEvent&lt;?&gt; event : key.pollEvents())</code> 循环将循环两次，且两次都能读取到完整的文件数据并创建正确的 <code>WebHookConfig</code>。这个行为是正确、符合预期的。</p><h3 id="Bug-原因"><a href="#Bug-原因" class="headerlink" title="Bug 原因"></a>Bug 原因</h3><p>当 JVM 获取文件系统事件的速度快于文件写入速度时，获取到的文件事件和数据就是不完整的；当 JVM 获取文件系统事件的速度慢于文件写入速度时，获取到的文件事件和数据才是完整的。</p><p>也就是说，当 <code>fileWatchRegister</code> 捕获到文件创建事件时，在没有添加延时的情况下，<code>key.pollEvents()</code> 只会返回一个 ENTRY_CREATE 事件，而没有等待足够的时间以使文件的内容完全写入磁盘。</p><p>理想情况下，文件系统事件是异步的，文件锁是同步的，加锁写入完成后，<code>fileWatchRegister</code> 应该继续捕获到 ENTRY_MODIFY 事件才对。没能继续监听，是因为 <code>cacheInit</code> 抛出的没有被捕获的异常向上传播，跳出了 <code>while</code> 循环，导致 <code>WatchKey</code> 没有被重置，<code>service.take()</code> 的调用被阻塞，无法获取新的事件。</p><p>然而，文件的写入速度是不可控的，添加固定的延时并不是一个稳妥的解决方法。我换了一台电脑，这个 bug 就没有完全复现。</p><h3 id="更好的解决方法"><a href="#更好的解决方法" class="headerlink" title="更好的解决方法"></a>更好的解决方法</h3><p>我的代码应该更具有鲁棒性，一个更好的解决方案是使用适当的文件监视事件来确保文件已经完全创建并写入，然后再执行操作。所以我监听文件的 “ENTRY_MODIFY” 事件，而不是在 “ENTRY_CREATE” 事件中执行 <code>cacheInit(file)</code>，这样可以更有把握地等待文件完全写入。</p><h2 id="在不同步的操作次数中同步共享缓存"><a href="#在不同步的操作次数中同步共享缓存" class="headerlink" title="在不同步的操作次数中同步共享缓存"></a>在不同步的操作次数中同步共享缓存</h2><p>PR 链接：<a href="https://github.com/apache/eventmesh/pull/4344#issuecomment-1673594283">https://github.com/apache/eventmesh/pull/4344#issuecomment-1673594283</a></p><h3 id="多线程的调试靠日志"><a href="#多线程的调试靠日志" class="headerlink" title="多线程的调试靠日志"></a>多线程的调试靠日志</h3><p>反复调用 <code>updateWebHookConfig</code> 端点时出现的 NPE：</p><p><img src="https://static.pil0txia.com/picgo/image-20230811171648705.webp"></p><p>调试模式下无法复现此 NPE。简单打几个日志，<code>fileWatchRegister</code> 获取到的是空文件，每次端点请求触发两次文件修改事件，第一次 NPE，第二次正常：</p><p><img src="https://static.pil0txia.com/picgo/image-20230810231100162.webp"></p><p>依然是简单地在获取事件前添加延时来解决问题，两次端点请求，每次请求只触发一次文件修改事件：</p><p><img src="https://static.pil0txia.com/picgo/image-20230810231556205.webp"></p><p>文件加锁、获取文件、最终写缓存和文件修改事件次数的详细日志。文件写入完毕后，触发第二次文件修改事件，此次正常工作：</p><p><img src="https://static.pil0txia.com/picgo/image-20230810235238507.webp"></p><h3 id="Bug-原因-1"><a href="#Bug-原因-1" class="headerlink" title="Bug 原因"></a>Bug 原因</h3><p>标注处可以直观的观察到，调用 <code>updateWebHookConfig</code> 端点后，在 <code>writeToFile</code> 中创建 <code>FileOutputStream</code> 时，触发了一次文件修改事件，<code>fileWatchRegister</code> 在文件加锁并写入前就获取了空文件，并调用 <code>cacheInit</code> 向 <code>cacheWebHookConfig</code> 写入了 <code>null</code>，导致 NPE：</p><p><img src="https://static.pil0txia.com/picgo/image-20230810235815341.webp"></p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>可以看到，在文件完成加锁前就已经抛出了 NPE，所以无法使用 <code>tryLock</code> 方法来判断文件状态。</p><p>此前，在插入配置时会产生 ENTRY_CREATE 和 ENTRY_MODIFY 事件，可以根据事件类型来区分对待；而更新配置时只产生 ENTRY_MODIFY 事件，与前者不同。因此，我使用 <code>CountDownLatch</code> 来通知文件写入已经完成，并在缓存初始化前等待文件写入完成的通知，即可解决问题。</p><p>不使用 <code>Semaphore</code>，而是 <code>CountDownLatch</code>，是因为 ENTRY_MODIFY 事件不止一次，而文件写入只有一次。只要第一次事件后能确保使用完整数据来更新缓存，后续的事件都是幂等的操作。</p><p>后续的事件发生时，<code>CountDownLatch</code> 将不会介入，此时由文件锁来加锁。文件锁在 try-with-resource 内部，<code>CountDownLatch</code> 在外部，是不冲突的。</p><blockquote><p>为什么不用非空判断呢？确实挺简单的，但是治标不治本。在这个问题修复之后，我记录的另一个问题也被顺便修复了。复现方法是在调用 <code>insertWebHookConfig</code> 端点后，紧接着调用 <code>deleteWebHookConfig</code> 端点，会抛出 NoSuchFile 异常：</p><p><img src="https://static.pil0txia.com/picgo/image-20230810220339265.webp"></p></blockquote><h2 id="思考题：在高并发下保证任务的有序性"><a href="#思考题：在高并发下保证任务的有序性" class="headerlink" title="思考题：在高并发下保证任务的有序性"></a>思考题：在高并发下保证任务的有序性</h2><p>PR 链接：<a href="https://github.com/apache/eventmesh/pull/4344#issuecomment-1681615940">https://github.com/apache/eventmesh/pull/4344#issuecomment-1681615940</a></p><p>我们修复了这个端点本身的功能，那并发呢？虽然作为一个配置端口，正常是不用考虑这么高的并发量的，不会这么频繁的更新删除配置。但反过来说，作为一个配置端口，在保证可靠性的前提下，并不需要很高的并发性能。</p><p>为了保证 countDown 操作的原子性，要不再加把锁？</p><p>高并发调用 <code>updateWebHookConfig</code> 端点时，如果立即调用 <code>deleteWebHookConfig</code> 端点，此时 ENTRY_MODIFY 事件栈还没有全部跑完、<code>cacheInit</code> 方法还未执行完毕，也就是说 pending 的更新配置的任务还没有全部完成，就提前执行了删除配置的任务。如何保证任务执行的严格有序性？要不再加个任务队列？</p><p>想到这里，我是越想越兴奋，但你可能会觉得：这只是一个接口而已 —— 是的，我们没有必要保证它的顺序一致性，只需要保证数据的最终一致性即可。既然管理员在某一时刻删掉了一个配置，那么，在此时刻前下发且预计在此时刻后执行的配置更新任务都将没有意义。</p><p>从代码层面讲，为了将 <code>file.isFile()</code> 和 <code>cacheInit(file)</code> 组合成一个原子操作，以避免 file 在通过 isFile () 判断后、阻塞在 <code>cacheInit(file)</code> 前时被删除，进而 <code>cacheInit</code> 时确保配置文件有效，只需要将 EVENT 的 dispatch 流程整体包裹在一个 synchronized 对象锁中即可，并使用此对象锁与 <code>writeToFile</code> 同步。</p><p>考虑到 <code>cacheInit</code> 方法中并没有使用传入的 file 来反序列化配置，而是再次使用了 BufferedReader 重新从磁盘读取文件，那么在 dispatch 流程初期 file 被实例化时是否包含了完整的文件数据并不重要，我们只需要使用它的 path。因此，实例化等更多层级就不需要被包裹在同步锁中，控制了锁的范围，保证了性能。</p><p>再仔细一想，既然 ENTRY_MODIFY 事件是在 <code>writeToFile</code> 中创建 <code>FileOutputStream</code> 时触发的，而后者已经被包裹在对象同步锁里了，也可以确保先完成写入文件再读取文件的顺序性，连 <code>CountDownLatch</code> 都可以省了。这也是同样的道理，尽管配置文件缓存的速度落后于配置文件磁盘 IO 的速度，但如果管理员在某一时刻想要更新一个配置，那么，在此时刻前下发且即将在此时刻后执行的缓存更新任务都是过期的。它们将会重复读取最新的配置文件，并执行幂等的写缓存操作。</p><p>大道至简。考虑到这个端点有限的并发量，目前的同步机制已经完全足够了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;在异步的文件系统事件中同步加锁写入&quot;&gt;&lt;a href=&quot;#在异步的文件系统事件中同步加锁写入&quot; class=&quot;headerlink&quot; title=&quot;在异步的文件系统事件中同步加锁写入&quot;&gt;&lt;/a&gt;在异步的文件系统事件中同步加锁写入&lt;/h2&gt;&lt;p&gt;PR 链接：&lt;a h</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="异步" scheme="https://www.pil0txia.com/tags/%E5%BC%82%E6%AD%A5/"/>
    
    <category term="同步" scheme="https://www.pil0txia.com/tags/%E5%90%8C%E6%AD%A5/"/>
    
    <category term="缓存" scheme="https://www.pil0txia.com/tags/%E7%BC%93%E5%AD%98/"/>
    
    <category term="锁" scheme="https://www.pil0txia.com/tags/%E9%94%81/"/>
    
    <category term="Webhook" scheme="https://www.pil0txia.com/tags/Webhook/"/>
    
    <category term="监听" scheme="https://www.pil0txia.com/tags/%E7%9B%91%E5%90%AC/"/>
    
  </entry>
  
  <entry>
    <title>【联想实习笔记】克服在流中使用 Lambda 表达式排序时编译器类型推断的弱点</title>
    <link href="https://www.pil0txia.com/post/2023-07-06_stream-comparator-and-lambda-sort/"/>
    <id>https://www.pil0txia.com/post/2023-07-06_stream-comparator-and-lambda-sort/</id>
    <published>2023-07-06T09:57:39.000Z</published>
    <updated>2023-07-06T10:27:39.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Motivation"><a href="#Motivation" class="headerlink" title="Motivation"></a>Motivation</h1><h2 id="Highlights"><a href="#Highlights" class="headerlink" title="Highlights"></a>Highlights</h2><p>使用 Lambda 表达式排序遇到的一个陷阱。在原有升序输出的流中新增<code>.reversed()</code> 方法、试图将流中的元素降序输出时，必须显式指定对象的类型。然而不使用<code>.reversed()</code> 方法时却不需要这么做，为什么？</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.sorted((m1, m2) -&gt; { <span class="keyword">return</span> Integer.valueOf(m1.get(<span class="string">"seq"</span>).toString()).compareTo(Integer.valueOf(m2.get(<span class="string">"seq"</span>).toString()));}) <span class="comment">// ok</span></span><br></pre></td></tr></tbody></table></figure><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.sorted(Comparator.comparingInt(m -&gt; Integer.valueOf(m.get(<span class="string">"seq"</span>).toString()))) <span class="comment">// ok</span></span><br></pre></td></tr></tbody></table></figure><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.sorted(Comparator.comparingInt(m -&gt; Integer.valueOf(m.get(<span class="string">"seq"</span>).toString())).reversed()) <span class="comment">// error</span></span><br></pre></td></tr></tbody></table></figure><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.sorted(Comparator.&lt;Map&gt;comparingInt(m -&gt; Integer.valueOf(m.get(<span class="string">"seq"</span>).toString())).reversed()) <span class="comment">// ok</span></span><br></pre></td></tr></tbody></table></figure><h2 id="Full-code-snippet"><a href="#Full-code-snippet" class="headerlink" title="Full code snippet"></a>Full code snippet</h2><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> List&lt;Map&lt;String, Object&gt;&gt; <span class="title function_">getFilterData</span><span class="params">(List&lt;ControlTowerFilterConf&gt; allByMenuIdAndUserIdOrderBySeq, <span class="type">boolean</span> dataFlag, <span class="type">long</span> menuId)</span> {</span><br><span class="line">    <span class="keyword">return</span> allByMenuIdAndUserIdOrderBySeq.parallelStream().map(controlTowerFilterConf -&gt; {</span><br><span class="line">            Map&lt;String, Object&gt; finalMap = Tool.entityToMap(controlTowerFilterConf);</span><br><span class="line">            ...</span><br><span class="line">            Arrays.asList(<span class="string">"buildDefaultValue"</span>, <span class="string">"buildData"</span>, <span class="string">"buildShowValue"</span>).stream().parallel().forEach(obj -&gt; {</span><br><span class="line">                ...</span><br><span class="line">            });</span><br><span class="line">            <span class="keyword">return</span> finalMap;</span><br><span class="line">        })</span><br><span class="line">        .sorted((m1, m2) -&gt; {</span><br><span class="line">        <span class="keyword">return</span> Integer.valueOf(m1.get(<span class="string">"seq"</span>).toString()).compareTo(Integer.valueOf(m2.get(<span class="string">"seq"</span>).toString()));</span><br><span class="line">    }).collect(Collectors.toList());</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h1 id="直接原因：类型接收者的参数中含有Lambda表达式"><a href="#直接原因：类型接收者的参数中含有Lambda表达式" class="headerlink" title="直接原因：类型接收者的参数中含有Lambda表达式"></a>直接原因：类型接收者的参数中含有 Lambda 表达式</h1><h2 id="English-Version"><a href="#English-Version" class="headerlink" title="English Version"></a>English Version</h2><h3 id="Case"><a href="#Case" class="headerlink" title="Case"></a>Case</h3><p>Sort the User objects list. Only works using method reference, with lambda expression the compiler gives an error:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; userList = Arrays.asList(u1, u2, u3);</span><br><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName())); <span class="comment">// works</span></span><br><span class="line">userList.sort(Comparator.comparing(User::getName).reversed()); <span class="comment">// works</span></span><br><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName()).reversed()); <span class="comment">// Compiler error</span></span><br></pre></td></tr></tbody></table></figure><p>Error:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">com\java8\collectionapi\CollectionTest.java:<span class="number">35</span>: error: cannot find symbol</span><br><span class="line">            userList.sort(Comparator.comparing(u -&gt; u.getName()).reversed());</span><br><span class="line">                                                     ^</span><br><span class="line">symbol:   method <span class="title function_">getName</span><span class="params">()</span></span><br><span class="line">location: variable u of type Object</span><br><span class="line"><span class="number">1</span> error</span><br></pre></td></tr></tbody></table></figure><h3 id="Reason"><a href="#Reason" class="headerlink" title="Reason"></a>Reason</h3><p>This is a weakness in the compiler’s type inferencing mechanism. In order to infer the type of <code>u</code> in the lambda, the <em>target type</em> for the lambda needs to be established. This is accomplished as follows. <code>userList.sort()</code> is expecting an argument of type <code>Comparator&lt;User&gt;</code>. In the first line, <code>Comparator.comparing()</code> needs to return <code>Comparator&lt;User&gt;</code>. This implies that <code>Comparator.comparing()</code> needs a <code>Function</code> that takes a <code>User</code> argument. Thus in the lambda on the first line, <code>u</code> must be of type <code>User</code> and everything works.</p><p>In the second and third lines, the target typing is disrupted by the presence of the call to <code>reversed()</code>. Both the receiver and the return type of <code>reversed()</code> are <code>Comparator&lt;T&gt;</code> so it seems like the target type should be propagated back to the receiver, but it isn’t. (Like I said, it’s a weakness.)</p><p>Lambdas are divided into <em>implicitly-typed</em> (no manifest types for parameters) and <em>explicitly-typed</em>; method references are divided into <em>exact</em> (no overloads) and <em>inexact</em>. When a generic method call in a <strong>receiver position</strong> has <strong>lambda arguments</strong>, and the type parameters cannot be fully inferred from the other arguments, you need to provide either an explicit lambda, an exact method ref, a target type cast, or explicit type witnesses for the generic method call to provide the additional type information needed to proceed.</p><p>In the second line, the method reference provides additional type information that fills this gap. This information is absent from the third line, so the compiler infers <code>u</code> to be <code>Object</code> (the inference fallback of last resort), which fails.</p><h3 id="Usage"><a href="#Usage" class="headerlink" title="Usage"></a>Usage</h3><p>This would work fine if you have not chained the comparators:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName());</span><br></pre></td></tr></tbody></table></figure><p>However, when comparators are chained, the type of the objects being compared need to be specified explicitly. Obviously if you can use a method reference, do that and it’ll work. Sometimes you can’t use a method reference,  for example when you reference non-static methods from static contexts.</p><p>If you want to pass an additional parameter, so you have to use a lambda expression. In that case you’d provide type in <code>comparing</code>:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.&lt;User&gt;comparing(u -&gt; u.getName()).reversed());</span><br></pre></td></tr></tbody></table></figure><p>or specify an explicit parameter type in the lambda:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.comparing((User u) -&gt; u.getName()).reversed());</span><br></pre></td></tr></tbody></table></figure><h2 id="中文版"><a href="#中文版" class="headerlink" title="中文版"></a>中文版</h2><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><p>对用户对象列表进行排序。只能使用方法引用，使用 lambda 表达式编译器会报错：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">List&lt;User&gt; userList = Arrays.asList(u1, u2, u3);</span><br><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName())); <span class="comment">// 正常工作</span></span><br><span class="line">userList.sort(Comparator.comparing(User::getName).reversed()); <span class="comment">// 正常工作</span></span><br><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName()).reversed()); <span class="comment">// 编译器错误</span></span><br></pre></td></tr></tbody></table></figure><p>错误信息：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">com\java8\collectionapi\CollectionTest.java:<span class="number">35</span>: error: cannot find symbol</span><br><span class="line">            userList.sort(Comparator.comparing(u -&gt; u.getName()).reversed());</span><br><span class="line">                                                     ^</span><br><span class="line">symbol:   method <span class="title function_">getName</span><span class="params">()</span></span><br><span class="line">location: variable u of type Object</span><br><span class="line"><span class="number">1</span> error</span><br></pre></td></tr></tbody></table></figure><h3 id="解释"><a href="#解释" class="headerlink" title="解释"></a>解释</h3><p>这是编译器的类型推断机制的一个弱点。为了推断 lambda 中 <code>u</code> 的类型，需要建立 lambda 的<em>目标类型</em>。具体过程是，<code>userList.sort()</code> 期望一个类型为 <code>Comparator&lt;User&gt;</code> 的参数。在第一行中，<code>Comparator.comparing()</code> 需要返回 <code>Comparator&lt;User&gt;</code>。这意味着 <code>Comparator.comparing()</code> 需要一个以 <code>User</code> 为参数的 <code>Function</code>。因此，在第一行的 lambda 表达式中，<code>u</code> 必须是 <code>User</code> 类型。</p><p>在第二行和第三行中，调用 <code>reversed()</code> 破坏了目标类型的推断。<code>reversed()</code> 的接收参数和返回类型都是 <code>Comparator&lt;T&gt;</code>，所以目标类型应该传播回接收者，但实际上没有传播。（弱点就在于此）</p><p>Lambda 分为<em>隐式类型</em>（参数没有明确的类型）和<em>显式类型</em>；方法引用分为<em>精确</em>（无重载）和<em>不精确</em>。当<strong>接收者位置</strong>的泛型方法调用具有 <strong>lambda 参数</strong>时，如果类型参数无法从其他参数中完全推断出来，就需要指定显式的 lambda、精确的方法引用、目标类型转换或用显式的泛型方法来调用类型见证 (type witnesses)，来为编译器提供所需的额外类型信息。</p><p>在第二行中，方法引用提供了填补这个间隙的额外类型信息。而第三行中缺少了这些信息，因此编译器将 <code>u</code> 推断为 <code>Object</code>（最后的推断回退父类），而 <code>Object</code> 并没有 <code>getName()</code> 方法，导致报错。</p><h3 id="正确用法"><a href="#正确用法" class="headerlink" title="正确用法"></a>正确用法</h3><p>如果你没有连续使用 Comparator，这样写就够了：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.comparing(u -&gt; u.getName());</span><br></pre></td></tr></tbody></table></figure><p>然而，当 Comparator 被连续拼接使用时，就需要明确指定被比较对象的类型。如果你可以用方法引用，那么这是最简洁的写法。不过有时并不能使用方法引用，比如在 static 上下文中引用非 static 方法。</p><p>如果你想传递额外的参数，那么必须使用 lambda 表达式。在这种情况下，你需要在 <code>comparing</code> 中显式提供类型：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.&lt;User&gt;comparing(u -&gt; u.getName()).reversed());</span><br></pre></td></tr></tbody></table></figure><p>或者在 lambda 中指定一个显式参数类型：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">userList.sort(Comparator.comparing((User u) -&gt; u.getName()).reversed());</span><br></pre></td></tr></tbody></table></figure><h1 id="间接原因：泛型类型擦除"><a href="#间接原因：泛型类型擦除" class="headerlink" title="间接原因：泛型类型擦除"></a>间接原因：泛型类型擦除</h1><h2 id="Java泛型的实现方法：类型擦除"><a href="#Java泛型的实现方法：类型擦除" class="headerlink" title="Java泛型的实现方法：类型擦除"></a>Java 泛型的实现方法：类型擦除</h2><p>大家都知道，Java 的泛型是伪泛型，这是因为 Java 在编译期间，所有的泛型信息都会被擦掉，正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的，在生成的字节码中是不包含泛型中的类型信息的，使用泛型的时候加上类型参数，在编译器编译的时候会去掉，这个过程成为<strong>类型擦除</strong>。</p><p>如在代码中定义 <code>List&lt;Object&gt;</code> 和 <code>List&lt;String&gt;</code> 等类型，在编译后都会变成 <code>List</code>，JVM 看到的只是 <code>List</code>，而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方，但是仍然无法在运行时刻出现的类型转换异常的情况，类型擦除也是 Java 的泛型与 C++ 模板机制实现方式之间的重要区别。</p><p>通过两个例子证明 Java 类型的类型擦除</p><h3 id="原始类型相等"><a href="#原始类型相等" class="headerlink" title="原始类型相等"></a>原始类型相等</h3><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> {</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"></span><br><span class="line">        ArrayList&lt;String&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();</span><br><span class="line">        list1.add(<span class="string">"abc"</span>);</span><br><span class="line"></span><br><span class="line">        ArrayList&lt;Integer&gt; list2 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;Integer&gt;();</span><br><span class="line">        list2.add(<span class="number">123</span>);</span><br><span class="line"></span><br><span class="line">        System.out.println(list1.getClass() == list2.getClass());</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>在这个例子中，我们定义了两个 <code>ArrayList</code> 数组，不过一个是 <code>ArrayList&lt;String&gt;</code> 泛型类型的，只能存储字符串；一个是 <code>ArrayList&lt;Integer&gt;</code> 泛型类型的，只能存储整数，最后，我们通过 <code>list1</code> 对象和 <code>list2</code> 对象的 <code>getClass()</code> 方法获取他们的类的信息，最后发现结果为 <code>true</code>。说明泛型类型 <code>String</code> 和 <code>Integer</code> 都被擦除掉了，只剩下原始类型。</p><h3 id="通过反射添加其它类型元素"><a href="#通过反射添加其它类型元素" class="headerlink" title="通过反射添加其它类型元素"></a>通过反射添加其它类型元素</h3><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> {</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"></span><br><span class="line">        ArrayList&lt;Integer&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;Integer&gt;();</span><br><span class="line"></span><br><span class="line">        list.add(<span class="number">1</span>);  <span class="comment">//这样调用 add 方法只能存储整形，因为泛型类型的实例为 Integer</span></span><br><span class="line"></span><br><span class="line">        list.getClass().getMethod(<span class="string">"add"</span>, Object.class).invoke(list, <span class="string">"asd"</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; list.size(); i++) {</span><br><span class="line">            System.out.println(list.get(i));</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>在程序中定义了一个 <code>ArrayList</code> 泛型类型实例化为 <code>Integer</code> 对象，如果直接调用 <code>add()</code> 方法，那么只能存储整数数据，不过当我们利用反射调用 <code>add()</code> 方法的时候，却可以存储字符串，这说明了 <code>Integer</code> 泛型实例在编译之后被擦除掉了，只保留了原始类型。</p><h2 id="类型擦除后保留的原始类型"><a href="#类型擦除后保留的原始类型" class="headerlink" title="类型擦除后保留的原始类型"></a>类型擦除后保留的原始类型</h2><p>在上面，两次提到了原始类型，什么是原始类型？</p><p><strong>原始类型</strong> 就是擦除去了泛型信息，最后在字节码中的类型变量的真正类型，无论何时定义一个泛型，相应的原始类型都会被自动提供，类型变量擦除，并使用其<strong>限定类型</strong>（无限定的变量用 Object）替换。</p><h3 id="原始类型Object"><a href="#原始类型Object" class="headerlink" title="原始类型Object"></a>原始类型 Object</h3><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Pair</span>&lt;T&gt; {  </span><br><span class="line">    <span class="keyword">private</span> T value;  </span><br><span class="line">    <span class="keyword">public</span> T <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> value;  </span><br><span class="line">    }  </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(T  value)</span> {  </span><br><span class="line">        <span class="built_in">this</span>.value = value;  </span><br><span class="line">    }  </span><br><span class="line">}  </span><br></pre></td></tr></tbody></table></figure><p>Pair 的原始类型为:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Pair</span> {  </span><br><span class="line">    <span class="keyword">private</span> Object value;  </span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> value;  </span><br><span class="line">    }  </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Object  value)</span> {  </span><br><span class="line">        <span class="built_in">this</span>.value = value;  </span><br><span class="line">    }  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>因为在 <code>Pair&lt;T&gt;</code> 中，T 是一个无限定的类型变量，所以用 <code>Object</code> 替换，其结果就是一个普通的类，如同泛型加入 Java 语言之前的已经实现的样子。在程序中可以包含不同类型的 <code>Pair</code>，如 <code>Pair&lt;String&gt;</code> 或 <code>Pair&lt;Integer&gt;</code>，但是擦除类型后他们的就成为原始的 <code>Pair</code> 类型了，原始类型都是 <code>Object</code>。</p><p>从上面的” 一 - 2” 中，我们也可以明白 <code>ArrayList&lt;Integer&gt;</code> 被擦除类型后，原始类型也变为 <code>Object</code>，所以通过反射我们就可以存储字符串了。</p><p>如果类型变量有限定，那么原始类型就用第一个边界的类型变量类替换。</p><p>比如: Pair 这样声明的话</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Pair</span>&lt;T <span class="keyword">extends</span> <span class="title class_">Comparable</span>&gt; {}</span><br></pre></td></tr></tbody></table></figure><p>那么原始类型就是 <code>Comparable</code>。</p><p>要区分<strong>原始类型</strong>和<strong>泛型变量的类型</strong>。</p><p>在调用泛型方法时，可以指定泛型，也可以不指定泛型。</p><ul><li>在不指定泛型的情况下，泛型变量的类型为该方法中的几种类型的同一父类的最小级，直到 Object。</li><li>在指定泛型的情况下，该方法的几种类型必须是该泛型的实例的类型或者其子类。</li></ul><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> {  </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {  </span><br><span class="line"></span><br><span class="line">        <span class="comment">/**不指定泛型的时候*/</span>  </span><br><span class="line">        <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> Test.add(<span class="number">1</span>, <span class="number">2</span>); <span class="comment">//这两个参数都是Integer，所以T为Integer类型  </span></span><br><span class="line">        <span class="type">Number</span> <span class="variable">f</span> <span class="operator">=</span> Test.add(<span class="number">1</span>, <span class="number">1.2</span>); <span class="comment">//这两个参数一个是Integer，以风格是Float，所以取同一父类的最小级，为Number  </span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">o</span> <span class="operator">=</span> Test.add(<span class="number">1</span>, <span class="string">"asd"</span>); <span class="comment">//这两个参数一个是Integer，以风格是Float，所以取同一父类的最小级，为Object  </span></span><br><span class="line"></span><br><span class="line">        <span class="comment">/**指定泛型的时候*/</span>  </span><br><span class="line">        <span class="type">int</span> <span class="variable">a</span> <span class="operator">=</span> Test.&lt;Integer&gt;add(<span class="number">1</span>, <span class="number">2</span>); <span class="comment">//指定了Integer，所以只能为Integer类型或者其子类  </span></span><br><span class="line">        <span class="type">int</span> <span class="variable">b</span> <span class="operator">=</span> Test.&lt;Integer&gt;add(<span class="number">1</span>, <span class="number">2.2</span>); <span class="comment">//编译错误，指定了Integer，不能为Float  </span></span><br><span class="line">        <span class="type">Number</span> <span class="variable">c</span> <span class="operator">=</span> Test.&lt;Number&gt;add(<span class="number">1</span>, <span class="number">2.2</span>); <span class="comment">//指定为Number，所以可以为Integer和Float  </span></span><br><span class="line">    }  </span><br><span class="line"></span><br><span class="line">    <span class="comment">//这是一个简单的泛型方法  </span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; T <span class="title function_">add</span><span class="params">(T x,T y)</span>{  </span><br><span class="line">        <span class="keyword">return</span> y;  </span><br><span class="line">    }  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>其实在泛型类中，不指定泛型的时候，也差不多，只不过这个时候的泛型为 <code>Object</code>，就比如 <code>ArrayList</code> 中，如果不指定泛型，那么这个 <code>ArrayList</code> 可以存储任意的对象。</p><h3 id="Object泛型"><a href="#Object泛型" class="headerlink" title="Object泛型"></a>Object 泛型</h3><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {  </span><br><span class="line">    <span class="type">ArrayList</span> <span class="variable">list</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>();  </span><br><span class="line">    list.add(<span class="number">1</span>);  </span><br><span class="line">    list.add(<span class="string">"121"</span>);  </span><br><span class="line">    list.add(<span class="keyword">new</span> <span class="title class_">Date</span>());  </span><br><span class="line">}  </span><br></pre></td></tr></tbody></table></figure><h2 id="类型擦除引起的问题及解决方法"><a href="#类型擦除引起的问题及解决方法" class="headerlink" title="类型擦除引起的问题及解决方法"></a>类型擦除引起的问题及解决方法</h2><p>因为种种原因，Java 不能实现真正的泛型，只能使用类型擦除来实现伪泛型，这样虽然不会有类型膨胀问题，但是也引起来许多新问题，所以，SUN 对这些问题做出了种种限制，避免我们发生各种错误。</p><h3 id="先检查再编译以及编译的对象和引用传递问题"><a href="#先检查再编译以及编译的对象和引用传递问题" class="headerlink" title="先检查再编译以及编译的对象和引用传递问题"></a>先检查再编译以及编译的对象和引用传递问题</h3><p><strong>Q</strong>: 既然说类型变量会在编译的时候擦除掉，那为什么我们往 ArrayList 创建的对象中添加整数会报错呢？不是说泛型变量 String 会在编译的时候变为 Object 类型吗？为什么不能存别的类型呢？既然类型擦除了，如何保证我们只能使用泛型变量限定的类型呢？</p><p><strong>A</strong>: Java 编译器是通过先检查代码中泛型的类型，然后在进行类型擦除，再进行编译。</p><p>例如：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span>  <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {  </span><br><span class="line"></span><br><span class="line">    ArrayList&lt;String&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();  </span><br><span class="line">    list.add(<span class="string">"123"</span>);  </span><br><span class="line">    list.add(<span class="number">123</span>);<span class="comment">//编译错误  </span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>在上面的程序中，使用 <code>add</code> 方法添加一个整型，在 IDE 中，直接会报错，说明这就是在编译之前的检查，因为如果是在编译之后检查，类型擦除后，原始类型为 <code>Object</code>，是应该允许任意引用类型添加的。可实际上却不是这样的，这恰恰说明了关于泛型变量的使用，是会在编译之前检查的。</p><p>那么，这个类型检查是针对谁的呢？我们先看看参数化类型和原始类型的兼容。</p><p>以 ArrayList 举例子，以前的写法:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ArrayList</span> <span class="variable">list</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>();  </span><br></pre></td></tr></tbody></table></figure><p>现在的写法:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;String&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();</span><br></pre></td></tr></tbody></table></figure><p>如果是与以前的代码兼容，各种引用传值之间，必然会出现如下的情况：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;String&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>(); <span class="comment">//第一种 情况</span></span><br><span class="line"><span class="type">ArrayList</span> <span class="variable">list2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;(); <span class="comment">//第二种 情况</span></span><br></pre></td></tr></tbody></table></figure><p>这样是没有错误的，不过会有个编译时警告。</p><p>不过在第一种情况，可以实现与完全使用泛型参数一样的效果，第二种则没有效果。</p><p>因为类型检查就是编译时完成的，<code>new ArrayList()</code> 只是在内存中开辟了一个存储空间，可以存储任何类型对象，而<strong>真正设计类型检查的是它的引用</strong>，因为我们是使用它引用 <code>list1</code> 来调用它的方法，比如说调用 <code>add</code> 方法，所以 <code>list1</code> 引用能完成泛型类型的检查。而引用 <code>list2</code> 没有使用泛型，所以不行。</p><p>举例子：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> {  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {  </span><br><span class="line"></span><br><span class="line">        ArrayList&lt;String&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>();  </span><br><span class="line">        list1.add(<span class="string">"1"</span>); <span class="comment">//编译通过  </span></span><br><span class="line">        list1.add(<span class="number">1</span>); <span class="comment">//编译错误  </span></span><br><span class="line">        <span class="type">String</span> <span class="variable">str1</span> <span class="operator">=</span> list1.get(<span class="number">0</span>); <span class="comment">//返回类型就是String  </span></span><br><span class="line"></span><br><span class="line">        <span class="type">ArrayList</span> <span class="variable">list2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();  </span><br><span class="line">        list2.add(<span class="string">"1"</span>); <span class="comment">//编译通过  </span></span><br><span class="line">        list2.add(<span class="number">1</span>); <span class="comment">//编译通过  </span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">object</span> <span class="operator">=</span> list2.get(<span class="number">0</span>); <span class="comment">//返回类型就是Object  </span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;().add(<span class="string">"11"</span>); <span class="comment">//编译通过  </span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;().add(<span class="number">22</span>); <span class="comment">//编译错误  </span></span><br><span class="line"></span><br><span class="line">        <span class="type">String</span> <span class="variable">str2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;().get(<span class="number">0</span>); <span class="comment">//返回类型就是String  </span></span><br><span class="line">    }  </span><br><span class="line"></span><br><span class="line">}  </span><br></pre></td></tr></tbody></table></figure><p>通过上面的例子，我们可以明白，<strong>类型检查就是针对引用的</strong>，谁是一个引用，用这个引用调用泛型方法，就会对这个引用调用的方法进行类型检测，而无关它真正引用的对象。</p><p>泛型中参数话类型为什么不考虑继承关系？</p><p>在 Java 中，像下面形式的引用传递是不允许的:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;String&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;Object&gt;(); <span class="comment">//编译错误  </span></span><br><span class="line">ArrayList&lt;Object&gt; list2 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;(); <span class="comment">//编译错误</span></span><br></pre></td></tr></tbody></table></figure><p>我们先看第一种情况，将第一种情况拓展成下面的形式：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;Object&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;Object&gt;();  </span><br><span class="line">list1.add(<span class="keyword">new</span> <span class="title class_">Object</span>());  </span><br><span class="line">list1.add(<span class="keyword">new</span> <span class="title class_">Object</span>());  </span><br><span class="line">ArrayList&lt;String&gt; list2 = list1; <span class="comment">//编译错误</span></span><br></pre></td></tr></tbody></table></figure><p>实际上，在第 4 行代码的时候，就会有编译错误。那么，我们先假设它编译没错。那么当我们使用 <code>list2</code> 引用用 <code>get()</code> 方法取值的时候，返回的都是 <code>String</code> 类型的对象（上面提到了，类型检测是根据引用来决定的），可是它里面实际上已经被我们存放了 <code>Object</code> 类型的对象，这样就会有 <code>ClassCastException</code> 了。所以为了避免这种极易出现的错误，Java 不允许进行这样的引用传递。（这也是泛型出现的原因，就是为了解决类型转换的问题，我们不能违背它的初衷）。</p><p>再看第二种情况，将第二种情况拓展成下面的形式：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;String&gt; list1 = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();  </span><br><span class="line">list1.add(<span class="keyword">new</span> <span class="title class_">String</span>());  </span><br><span class="line">list1.add(<span class="keyword">new</span> <span class="title class_">String</span>());</span><br><span class="line"></span><br><span class="line">ArrayList&lt;Object&gt; list2 = list1; <span class="comment">//编译错误</span></span><br></pre></td></tr></tbody></table></figure><p>没错，这样的情况比第一种情况好的多，最起码，在我们用 <code>list2</code> 取值的时候不会出现 <code>ClassCastException</code>，因为是从 <code>String</code> 转换为 <code>Object</code>。可是，这样做有什么意义呢，泛型出现的原因，就是为了解决类型转换的问题。我们使用了泛型，到头来，还是要自己强转，违背了泛型设计的初衷。所以 java 不允许这么干。再说，你如果又用 <code>list2</code> 往里面 <code>add()</code> 新的对象，那么到时候取得时候，我怎么知道我取出来的到底是 <code>String</code> 类型的，还是 <code>Object</code> 类型的呢？</p><p><strong>所以，要格外注意，泛型中的引用传递的问题。</strong></p><h3 id="自动类型转换"><a href="#自动类型转换" class="headerlink" title="自动类型转换"></a>自动类型转换</h3><p>因为类型擦除的问题，所以所有的泛型类型变量最后都会被替换为原始类型。</p><p>既然都被替换为原始类型，那么为什么我们在获取的时候，不需要进行强制类型转换呢？</p><p>看下 <code>ArrayList.get()</code> 方法：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> E <span class="title function_">get</span><span class="params">(<span class="type">int</span> index)</span> {  </span><br><span class="line"></span><br><span class="line">    RangeCheck(index);  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (E) elementData[index];  </span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>可以看到，在 <code>return</code> 之前，会根据泛型变量进行强转。假设泛型类型变量为 <code>Date</code>，虽然泛型信息会被擦除掉，但是会将 <code>(E) elementData[index]</code>，编译为 <code>(Date) elementData[index]</code>。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设 <code>Pair</code> 类的 <code>value</code> 域是 <code>public</code> 的，那么表达式：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Date</span> <span class="variable">date</span> <span class="operator">=</span> pair.value;</span><br></pre></td></tr></tbody></table></figure><p>也会自动地在结果字节码中插入强制类型转换。</p><h3 id="类型擦除与多态的冲突和解决方法"><a href="#类型擦除与多态的冲突和解决方法" class="headerlink" title="类型擦除与多态的冲突和解决方法"></a>类型擦除与多态的冲突和解决方法</h3><p>现在有这样一个泛型类：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Pair</span>&lt;T&gt; {  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> T value;  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> T <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> value;  </span><br><span class="line">    }  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(T value)</span> {  </span><br><span class="line">        <span class="built_in">this</span>.value = value;  </span><br><span class="line">    }  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>然后我们想要一个子类继承它。</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">DateInter</span> <span class="keyword">extends</span> <span class="title class_">Pair</span>&lt;Date&gt; {  </span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Date value)</span> {  </span><br><span class="line">        <span class="built_in">super</span>.setValue(value);  </span><br><span class="line">    }  </span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Date <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">super</span>.getValue();  </span><br><span class="line">    }  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>在这个子类中，我们设定父类的泛型类型为 <code>Pair&lt;Date&gt;</code>，在子类中，我们覆盖了父类的两个方法，我们的原意是这样的：将父类的泛型类型限定为 <code>Date</code>，那么父类里面的两个方法的参数都为 <code>Date</code> 类型。</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Date <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">    <span class="keyword">return</span> value;  </span><br><span class="line">}  </span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Date value)</span> {  </span><br><span class="line">    <span class="built_in">this</span>.value = value;  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>所以，我们在子类中重写这两个方法一点问题也没有，实际上，从他们的 <code>@Override</code> 标签中也可以看到，一点问题也没有，实际上是这样的吗？</p><p>分析：实际上，类型擦除后，父类的的泛型类型全部变为了原始类型 <code>Object</code>，所以父类编译之后会变成下面的样子：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Pair</span> {  </span><br><span class="line">    <span class="keyword">private</span> Object value;  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> value;  </span><br><span class="line">    }  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Object  value)</span> {  </span><br><span class="line">        <span class="built_in">this</span>.value = value;  </span><br><span class="line">    }  </span><br><span class="line">}  </span><br></pre></td></tr></tbody></table></figure><p>再看子类的两个重写的方法的类型：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span>  </span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Date value)</span> {  </span><br><span class="line">    <span class="built_in">super</span>.setValue(value);  </span><br><span class="line">}  </span><br><span class="line"><span class="meta">@Override</span>  </span><br><span class="line"><span class="keyword">public</span> Date <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">super</span>.getValue();  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>先来分析 <code>setValue</code> 方法，父类的类型是 <code>Object</code>，而子类的类型是 <code>Date</code>，参数类型不一样，这如果实在普通的继承关系中，根本就不会是重写，而是重载。</p><p>我们在一个 main 方法测试一下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ClassNotFoundException {  </span><br><span class="line">        <span class="type">DateInter</span> <span class="variable">dateInter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DateInter</span>();  </span><br><span class="line">        dateInter.setValue(<span class="keyword">new</span> <span class="title class_">Date</span>());                  </span><br><span class="line">        dateInter.setValue(<span class="keyword">new</span> <span class="title class_">Object</span>()); <span class="comment">//编译错误  </span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果是重载，那么子类中两个 <code>setValue</code> 方法，一个是参数 <code>Object</code> 类型，一个是 <code>Date</code> 类型，可是我们发现，根本就没有这样的一个子类继承自父类的 Object 类型参数的方法。所以说，却是是重写了，而不是重载了。</p><p>为什么会这样呢？</p><p>原因是这样的，我们传入父类的泛型类型是 <code>Date，Pair&lt;Date&gt;</code>，我们的本意是将泛型类变为如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Pair</span> {  </span><br><span class="line">    <span class="keyword">private</span> Date value;  </span><br><span class="line">    <span class="keyword">public</span> Date <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">        <span class="keyword">return</span> value;  </span><br><span class="line">    }  </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(Date value)</span> {  </span><br><span class="line">        <span class="built_in">this</span>.value = value;  </span><br><span class="line">    }  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>然后再子类中重写参数类型为 Date 的那两个方法，实现继承中的多态。</p><p>可是由于种种原因，虚拟机并不能将泛型类型变为 <code>Date</code>，只能将类型擦除掉，变为原始类型 <code>Object</code>。这样，我们的本意是进行重写，实现多态。可是类型擦除后，只能变为了重载。这样，类型擦除就和多态有了冲突。JVM 知道你的本意吗？知道！！！可是它能直接实现吗，不能！！！如果真的不能的话，那我们怎么去重写我们想要的 <code>Date</code> 类型参数的方法啊。</p><p>于是 JVM 采用了一个特殊的方法，来完成这项功能，那就是<strong>桥方法</strong>。</p><p>首先，我们用 <code>javap -c className</code> 的方式反编译下 <code>DateInter</code> 子类的字节码，结果如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">com</span>.tao.test.DateInter <span class="keyword">extends</span> <span class="title class_">com</span>.tao.test.Pair&lt;java.util.Date&gt; {  </span><br><span class="line">  com.tao.test.DateInter();  </span><br><span class="line">    Code:  </span><br><span class="line">       <span class="number">0</span>: aload_0  </span><br><span class="line">       <span class="number">1</span>: invokespecial #<span class="number">8</span>                  <span class="comment">// Method com/tao/test/Pair."&lt;init&gt;":()V  </span></span><br><span class="line">       <span class="number">4</span>: <span class="keyword">return</span>  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(java.util.Date)</span>;  <span class="comment">//我们重写的setValue方法  </span></span><br><span class="line">    Code:  </span><br><span class="line">       <span class="number">0</span>: aload_0  </span><br><span class="line">       <span class="number">1</span>: aload_1  </span><br><span class="line">       <span class="number">2</span>: invokespecial #<span class="number">16</span>                 <span class="comment">// Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  </span></span><br><span class="line">       <span class="number">5</span>: <span class="keyword">return</span>  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> java.util.Date <span class="title function_">getValue</span><span class="params">()</span>;    <span class="comment">//我们重写的getValue方法  </span></span><br><span class="line">    Code:  </span><br><span class="line">       <span class="number">0</span>: aload_0  </span><br><span class="line">       <span class="number">1</span>: invokespecial #<span class="number">23</span>                 <span class="comment">// Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  </span></span><br><span class="line">       <span class="number">4</span>: checkcast     #<span class="number">26</span>                 <span class="comment">// class java/util/Date  </span></span><br><span class="line">       <span class="number">7</span>: areturn  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> java.lang.Object <span class="title function_">getValue</span><span class="params">()</span>;     <span class="comment">//编译时由编译器生成的桥方法  </span></span><br><span class="line">    Code:  </span><br><span class="line">       <span class="number">0</span>: aload_0  </span><br><span class="line">       <span class="number">1</span>: invokevirtual #<span class="number">28</span>                 <span class="comment">// Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  </span></span><br><span class="line">       <span class="number">4</span>: areturn  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setValue</span><span class="params">(java.lang.Object)</span>;   <span class="comment">//编译时由编译器生成的桥方法  </span></span><br><span class="line">    Code:  </span><br><span class="line">       <span class="number">0</span>: aload_0  </span><br><span class="line">       <span class="number">1</span>: aload_1  </span><br><span class="line">       <span class="number">2</span>: checkcast     #<span class="number">26</span>                 <span class="comment">// class java/util/Date  </span></span><br><span class="line">       <span class="number">5</span>: invokevirtual #<span class="number">30</span>                 <span class="comment">// Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  </span></span><br><span class="line">       <span class="number">8</span>: <span class="keyword">return</span>  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>从编译的结果来看，我们本意重写 <code>setValue</code> 和 <code>getValue</code> 方法的子类，竟然有 4 个方法，其实不用惊奇，最后的两个方法，就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是 Object，也就是说，子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的 <code>setvalue</code> 和 <code>getValue</code> 方法上面的 <code>@Oveerride</code> 只不过是假象。而桥方法的内部实现，就只是去调用我们自己重写的那两个方法。</p><p>所以，<strong>虚拟机巧妙的使用了桥方法，来解决了类型擦除和多态的冲突</strong>。</p><p>不过，要提到一点，这里面的 <code>setValue</code> 和 <code>getValue</code> 这两个桥方法的意义又有不同。</p><p><code>setValue</code> 方法是为了解决类型擦除与多态之间的冲突。</p><p>而 <code>getValue</code> 却有普遍的意义，怎么说呢，如果这是一个普通的继承关系：</p><p>那么父类的 <code>getValue</code> 方法如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Object <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">    <span class="keyword">return</span> value;  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>而子类重写的方法是：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Date <span class="title function_">getValue</span><span class="params">()</span> {  </span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">super</span>.getValue();  </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>其实这在普通的类继承中也是普遍存在的重写，这就是协变。</p><p>关于协变：。。。。。。</p><p>并且，还有一点也许会有疑问，子类中的桥方法 <code>Object getValue()</code> 和 <code>Date getValue()</code> 是同时存在的，可是如果是常规的两个方法，他们的方法签名是一样的，也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写 Java 代码，这样的代码是无法通过编译器的检查的，但是虚拟机却是允许这样做的，因为虚拟机通过参数类型和返回类型来确定一个方法，所以编译器为了实现泛型的多态允许自己做这个看起来 “不合法” 的事情，然后交给虚拟器去区别。</p><h3 id="泛型类型变量不能是基本数据类型"><a href="#泛型类型变量不能是基本数据类型" class="headerlink" title="泛型类型变量不能是基本数据类型"></a>泛型类型变量不能是基本数据类型</h3><p>不能用类型参数替换基本类型。就比如，没有 <code>ArrayList&lt;double&gt;</code>，只有 <code>ArrayList&lt;Double&gt;</code>。因为当类型擦除后，<code>ArrayList</code> 的原始类型变为 <code>Object</code>，但是 <code>Object</code> 类型不能存储 <code>double</code> 值，只能引用 <code>Double</code> 的值。</p><h3 id="编译时集合的instanceof"><a href="#编译时集合的instanceof" class="headerlink" title="编译时集合的instanceof"></a>编译时集合的 instanceof</h3><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ArrayList&lt;String&gt; arrayList = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();</span><br></pre></td></tr></tbody></table></figure><p>因为类型擦除之后，<code>ArrayList&lt;String&gt;</code> 只剩下原始类型，泛型信息 <code>String</code> 不存在了。</p><p>那么，编译时进行类型查询的时候使用下面的方法是错误的</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>( arrayList <span class="keyword">instanceof</span> ArrayList&lt;String&gt;)</span><br></pre></td></tr></tbody></table></figure><h3 id="泛型在静态方法和静态类中的问题"><a href="#泛型在静态方法和静态类中的问题" class="headerlink" title="泛型在静态方法和静态类中的问题"></a>泛型在静态方法和静态类中的问题</h3><p>泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数</p><p>举例说明：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test2</span>&lt;T&gt; {    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> T one;   <span class="comment">//编译错误    </span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span>  T <span class="title function_">show</span><span class="params">(T one)</span>{ <span class="comment">//编译错误    </span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;    </span><br><span class="line">    }    </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>因为泛型类中的泛型参数的实例化是在定义对象的时候指定的，而静态变量和静态方法不需要使用对象来调用。对象都没有创建，如何确定这个泛型参数是何种类型，所以当然是错误的。</p><p>但是要注意区分下面的一种情况：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test2</span>&lt;T&gt; {    </span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T &gt;T <span class="title function_">show</span><span class="params">(T one)</span>{ <span class="comment">//这是正确的    </span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;    </span><br><span class="line">    }    </span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>因为这是一个泛型方法，在泛型方法中使用的 T 是自己在方法中定义的 T，而不是泛型类中的 T。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><blockquote><p><a href="https://stackoverflow.com/questions/25172595/comparator-reversed-does-not-compile-using-lambda">java - Comparator.reversed() does not compile using lambda - Stack Overflow</a></p><p><a href="https://stackoverflow.com/questions/68678263/how-to-use-comparator-in-java-with-lambda-expression">How to use Comparator in java with lambda expression? - Stack Overflow</a></p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Motivation&quot;&gt;&lt;a href=&quot;#Motivation&quot; class=&quot;headerlink&quot; title=&quot;Motivation&quot;&gt;&lt;/a&gt;Motivation&lt;/h1&gt;&lt;h2 id=&quot;Highlights&quot;&gt;&lt;a href=&quot;#Highlights&quot;</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="实习" scheme="https://www.pil0txia.com/tags/%E5%AE%9E%E4%B9%A0/"/>
    
    <category term="排序" scheme="https://www.pil0txia.com/tags/%E6%8E%92%E5%BA%8F/"/>
    
    <category term="Stream" scheme="https://www.pil0txia.com/tags/Stream/"/>
    
    <category term="Lambda" scheme="https://www.pil0txia.com/tags/Lambda/"/>
    
    <category term="Comparator" scheme="https://www.pil0txia.com/tags/Comparator/"/>
    
  </entry>
  
  <entry>
    <title>【开源贡献笔记】前辈在单元测试里留下的 TODO 注释，你该信吗？</title>
    <link href="https://www.pil0txia.com/post/2023-06-27_do-not-trust-todo-comment/"/>
    <id>https://www.pil0txia.com/post/2023-06-27_do-not-trust-todo-comment/</id>
    <published>2023-06-27T12:47:54.000Z</published>
    <updated>2023-06-27T16:35:54.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Take-a-look"><a href="#Take-a-look" class="headerlink" title="Take a look"></a>Take a look</h2><p>记录一种”Looks Good”，甚至单测也能跑通，但实际上起不到作用的单元测试写法。</p><p>PR 链接（Merged）：<a href="https://github.com/apache/eventmesh/pull/4139">https://github.com/apache/eventmesh/pull/4139</a></p><p>这是 UrlMappingPattern 工具类中的 compile 方法：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">compile</span><span class="params">()</span> {</span><br><span class="line">    acquireParamNames();</span><br><span class="line">    <span class="type">String</span> <span class="variable">parsedPattern</span> <span class="operator">=</span> urlMappingPattern.replaceFirst(URL_FORMAT_REGEX, URL_FORMAT_MATCH_REGEX);</span><br><span class="line">    parsedPattern = parsedPattern.replaceAll(URL_PARAMETER_REGEX, URL_PARAMETER_MATCH_REGEX);</span><br><span class="line">    <span class="built_in">this</span>.compiledUrlMappingPattern = Pattern.compile(parsedPattern + URL_QUERY_STRING_REGEX);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>这是现有的 UrlMappingPatternTest 测试类：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UrlMappingPatternTest</span> {</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">TEST_URL_MAPPING_PATTERN</span> <span class="operator">=</span> <span class="string">"/test/{param1}/path/{param2}"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> TestUrlMappingPattern urlMappingPattern;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Before</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> {</span><br><span class="line">        urlMappingPattern = <span class="keyword">new</span> <span class="title class_">TestUrlMappingPattern</span>(TEST_URL_MAPPING_PATTERN);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGetMappingPattern</span><span class="params">()</span> {</span><br><span class="line">        assertEquals(<span class="string">"/test/{param1}/path/{param2}"</span>, urlMappingPattern.getMappingPattern());</span><br><span class="line">    }</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCompile</span><span class="params">()</span> <span class="keyword">throws</span> NoSuchFieldException, IllegalAccessException {</span><br><span class="line">        <span class="comment">//TODO : Fix me to test the method compile(). It is better using Mockito not PowerMockito.</span></span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">class</span> <span class="title class_">TestUrlMappingPattern</span> <span class="keyword">extends</span> <span class="title class_">UrlMappingPattern</span> {</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> Pattern compiledUrlMappingPattern;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="title function_">TestUrlMappingPattern</span><span class="params">(String pattern)</span> {</span><br><span class="line">            <span class="built_in">super</span>(pattern);</span><br><span class="line">            compiledUrlMappingPattern = mock(Pattern.class);</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>GPT 会在私有字段的获取、子类的归属和正则表达式的替换上犯很多错误，这时候就不能帮我们省事了。前辈留下的注释说要用 Mockito，GPT 就给出了 <code>Mockito.verify</code> 方法，然后在此基础上加上反射和正确的正则表达式，于是 testCompile () 就可以跑通测试方法了。乍一看似乎没什么问题：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCompile</span><span class="params">()</span> <span class="keyword">throws</span> NoSuchFieldException, IllegalAccessException {</span><br><span class="line">    <span class="comment">// Obtain compiledUrlMappingPattern field with reflection</span></span><br><span class="line">        <span class="type">Field</span> <span class="variable">compiledUrlMappingPatternField</span> <span class="operator">=</span> UrlMappingPattern.class.getDeclaredField(<span class="string">"compiledUrlMappingPattern"</span>);</span><br><span class="line">        compiledUrlMappingPatternField.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    urlMappingPattern.compile();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Verify that the compiledUrlMappingPattern field is updated</span></span><br><span class="line">        <span class="type">Pattern</span> <span class="variable">compiledPattern</span> <span class="operator">=</span> (Pattern) compiledUrlMappingPatternField.get(urlMappingPattern);</span><br><span class="line">        assertNotNull(compiledPattern);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Verify that the mocked pattern is compiled with the expected regex</span></span><br><span class="line">    Mockito.verify(urlMappingPattern.compiledUrlMappingPattern)</span><br><span class="line">        .compile(<span class="string">"/test/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)/path/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)(?:\\?.*?)?$"</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>但是跑测试类时，会使别的测试方法报错（虽然报错信息依然指向这里）：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">org.mockito.exceptions.misusing.UnfinishedVerificationException: </span><br><span class="line">Missing method call for verify(mock) here:</span><br><span class="line">-&gt; at org.apache.eventmesh.admin.rocketmq.util.UrlMappingPatternTest.testCompile(UrlMappingPatternTest.java:95)</span><br><span class="line"></span><br><span class="line">Example of correct verification:</span><br><span class="line">    verify(mock).doSomething()</span><br><span class="line"></span><br><span class="line">Also, this error might show up because you verify either of: final/private/equals()/hashCode() methods.</span><br><span class="line">Those methods *cannot* be stubbed/verified.</span><br><span class="line">Mocking methods declared on non-public parent classes is not supported.</span><br><span class="line"></span><br><span class="line">at org.apache.eventmesh.admin.rocketmq.util.UrlMappingPatternTest$TestUrlMappingPattern.&lt;init&gt;(UrlMappingPatternTest.java:105)</span><br><span class="line">at org.apache.eventmesh.admin.rocketmq.util.UrlMappingPatternTest.setUp(UrlMappingPatternTest.java:44)</span><br></pre></td></tr></tbody></table></figure><h2 id="原因分析"><a href="#原因分析" class="headerlink" title="原因分析"></a>原因分析</h2><p>首先，想要使用 <code>Mockito.verify</code> 来验证是否使用预期的参数调用了指定方法，其验证的对象必须是一个 mock 对象。<code>UrlMappingPatternTest</code> 测试类的 <code>TestUrlMappingPattern</code> 子类中提供了一个已经被 mock 过的对象。</p><p>mock 的主要作用是模拟对象预期的行为，而这里只需要将预期的值与实际的值相比较即可，不需要模拟行为，所以只需要利用反射获取 <code>UrlMappingPattern</code> 类中的私有字段即可，然后用 <code>assertEquals</code> 断言判断。</p><p>但是，因为子类中继承了超类的构造方法、mock 了 <code>compiledUrlMappingPattern</code> 并且在 <code>UrlMappingPatternTest</code> 测试类中被实例化为 <code>urlMappingPattern</code>，所以，在反射中使用 <code>urlMappingPattern.getclass()</code>，获取到的将是 <code>TestUrlMappingPattern</code> 子类中 mock 的 <code>compiledUrlMappingPattern</code> 字段，而该 mock 字段是没有初始化的，不应该被用作比较。从这里可以看出报错信息是不够准确的。</p><p>正确的做法是在反射中使用 <code>UrlMappingPattern.class</code>，这样获取的才是实际的 <code>compiledUrlMappingPattern</code>。</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCompile</span><span class="params">()</span> <span class="keyword">throws</span> NoSuchFieldException, IllegalAccessException {</span><br><span class="line">        <span class="comment">// Obtain compiledUrlMappingPattern field with reflection</span></span><br><span class="line">        <span class="type">Field</span> <span class="variable">compiledUrlMappingPatternField</span> <span class="operator">=</span> UrlMappingPattern.class.getDeclaredField(<span class="string">"compiledUrlMappingPattern"</span>);</span><br><span class="line">        compiledUrlMappingPatternField.setAccessible(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">        urlMappingPattern.compile();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Verify that the compiledUrlMappingPattern field is updated</span></span><br><span class="line">        <span class="type">Pattern</span> <span class="variable">compiledPattern</span> <span class="operator">=</span> (Pattern) compiledUrlMappingPatternField.get(urlMappingPattern);</span><br><span class="line">        assertNotNull(compiledPattern);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Verify that the mocked pattern is compiled with the expected regex</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">expectedRegex</span> <span class="operator">=</span> <span class="string">"/test/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)/path/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)(?:\\?.*?)?$"</span>;</span><br><span class="line">        <span class="type">Pattern</span> <span class="variable">expectedPattern</span> <span class="operator">=</span> Pattern.compile(expectedRegex);</span><br><span class="line">        assertEquals(expectedPattern.pattern(), compiledPattern.pattern());</span><br><span class="line">    }</span><br></pre></td></tr></tbody></table></figure><h2 id="点题"><a href="#点题" class="headerlink" title="点题"></a>点题</h2><p>那么话说回来，这最后不是完全没用上 Mockito 吗？</p><p>是的，确实用不上，也没有用它的理由。代码库里的注释很重要，但也不要被误导了。</p><p>如果你一定要用 Mockito，当然也可以，但你必须要让 <code>urlMappingPattern.compiledUrlMappingPattern</code> 返回预期的结果，所以你只能在 TestUrlMappingPattern 子类中重写 compile 方法，也不得不把私有的 acquireParamNames 方法和字符串常量临时标记为 public：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">compile</span><span class="params">()</span> {</span><br><span class="line">    acquireParamNames();</span><br><span class="line">    <span class="type">String</span> <span class="variable">parsedPattern</span> <span class="operator">=</span> getMappingPattern().replaceFirst(URL_FORMAT_REGEX, URL_FORMAT_MATCH_REGEX);</span><br><span class="line">    parsedPattern = parsedPattern.replaceAll(URL_PARAMETER_REGEX, URL_PARAMETER_MATCH_REGEX);</span><br><span class="line">    <span class="built_in">this</span>.compiledUrlMappingPattern = Mockito.mock(Pattern.class);</span><br><span class="line">    Mockito.when(compiledUrlMappingPattern.pattern()).thenReturn(parsedPattern + URL_QUERY_STRING_REGEX);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>于是，testCompile 测试方法通过了，但其它的测试方法又报错失败了，因为你给它们引用的 <code>urlMappingPattern.compiledUrlMappingPattern</code> 制造了额外的行为。</p><p>现在你还想继续按注释说的做吗？😉</p><hr><p>最后，感谢为我 Review 并提出宝贵意见的贡献者们，在 PR 下思维的碰撞是一件很令人喜悦的事情。</p><p><img src="https://static.pil0txia.com/picgo/image-20230628003145577.webp" alt="image-20230628003145577"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;Take-a-look&quot;&gt;&lt;a href=&quot;#Take-a-look&quot; class=&quot;headerlink&quot; title=&quot;Take a look&quot;&gt;&lt;/a&gt;Take a look&lt;/h2&gt;&lt;p&gt;记录一种”Looks Good”，甚至单测也能跑通，但实际上起不到作</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="测试" scheme="https://www.pil0txia.com/tags/%E6%B5%8B%E8%AF%95/"/>
    
    <category term="Mockito" scheme="https://www.pil0txia.com/tags/Mockito/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
  </entry>
  
  <entry>
    <title>【开源之夏】DataSphereStudio 集成 GitLab 课题 Proposal（已中选）</title>
    <link href="https://www.pil0txia.com/post/2023-06-23_glcc-2023-dss/"/>
    <id>https://www.pil0txia.com/post/2023-06-23_glcc-2023-dss/</id>
    <published>2023-06-23T13:42:30.000Z</published>
    <updated>2023-06-27T15:59:30.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>6-27 更新：已中选，虽然最多可以申请两个课题，但很遗憾 GLCC 规定学生只能参与第一个课题。导师对我很好，会继续关注 WeDataSphere 社区的！</p><p><img src="https://static.pil0txia.com/picgo/image-20230627234718665.webp"></p><p><img src="https://static.pil0txia.com/picgo/image-20230627234804484.webp"></p><p><img src="https://static.pil0txia.com/picgo/image-20230628114500453.webp" alt="两个课题的passStatus字段都为true"></p><p>此项目托付给了我的同学杨朋睿。</p></blockquote><h1 id="项目申请书"><a href="#项目申请书" class="headerlink" title="项目申请书"></a>项目申请书</h1><h2 id="课题名称"><a href="#课题名称" class="headerlink" title="课题名称"></a>课题名称</h2><p><a href="https://www.gitlink.org.cn/glcc/2023/subjects/detail/682">DataSphereStudio 集成 Gitlab</a></p><p>申请人：夏天</p><p>导师：张旗 | <a href="mailto:burdezhang@webank.com">burdezhang@webank.com</a></p><h2 id="社区简介"><a href="#社区简介" class="headerlink" title="社区简介"></a>社区简介</h2><p>DataSphere Studio（简称 DSS）是微众银行自研的一站式数据应用开发管理框架。</p><p>​    在统一的 UI 下，DataSphere Studio 以工作流式的图形化拖拽开发体验，将满足从数据交换、脱敏清洗、分析挖掘、质量检测、可视化展现、定时调度到数据输出应用等，数据应用开发全流程场景需求。</p><p>​    <strong>DSS 通过插拔式的集成框架设计，让用户可以根据需要，简单快速替换 DSS 已集成的各种功能组件，或新增功能组件。</strong></p><h2 id="题目简介"><a href="#题目简介" class="headerlink" title="题目简介"></a>题目简介</h2><p>本课题旨在将 Gitlab 集成到 DataSphereStudio 中，以便于更好地管理用户在 DataSphereStudio 中开发的代码。</p><ol><li>接入 Gitlab：我们将实现 DataSphereStudio 中的脚本接入 Gitlab 的功能，以便使用 DataSphereStudio 的开发人员可以更好地查看、修改和管理代码。这将有助于提升协作效率和代码质量。</li><li>版本管理和协作：通过集成 Gitlab，我们将实现 DataSphereStudio 中的代码版本管理和团队协作功能。学生们将有机会学习到 DataSphereStudio 集成三方应用的框架能力，同时能够掌握 Git 的基本概念，如代码提交、分支管理和代码合并，以及通过 Gitlab 进行代码审查和讨论。</li></ol><h2 id="编码任务"><a href="#编码任务" class="headerlink" title="编码任务"></a>编码任务</h2><ul><li>开发 dss-gitlab-appconn 模块，通过 DSS 的 AppConn 对接规范，将 GitLab 作为第三方系统，以插件的形式接入 DSS</li><li>dss-gitlab-appconn 模块需要提供初始化 (git init)、克隆 (git clone)、拉取 (git pull)、推送 (git push)、获取 (git fetch)、提交 (git commit)、合并 (git merge)、变基 (git rebase) 等常见 Git 版本管理操作的 API 接口，并以有效的数据格式返回给前端</li><li>采用 CheckStyle 代码风格，为 dss-gitlab-appconn 模块的接口编写 JavaDoc 注释</li><li>在官方 DataSphereStudio-Doc 文档仓库中编写 dss-gitlab-appconn 模块的接口文档</li><li>测试接口功能并完善优化</li></ul><p>如果上述任务完成进度快于预期，可以在活动结束前和后续社区贡献中继续完成其它任务。</p><h2 id="架构分析"><a href="#架构分析" class="headerlink" title="架构分析"></a>架构分析</h2><h3 id="Apache-Linkis-解耦架构"><a href="#Apache-Linkis-解耦架构" class="headerlink" title="Apache Linkis 解耦架构"></a>Apache Linkis 解耦架构</h3><p>Apache Linkis 可以在大数据领域将应用与基础设施解耦，其解耦原理与另一开源项目 Apache EventMesh 类似，后者可以将应用中的业务逻辑与事件存储的强绑定解耦，两者都使用了 sink connector 和 source connector，以插件形式提供对不同基础设施的支持能力。</p><p><img src="https://static.pil0txia.com/picgo/image-20230613214715150.webp" alt="image-20230613214715150"></p><p><img src="https://static.pil0txia.com/picgo/image-20230613214912291.webp" alt="image-20230613214912291"></p><p>Apache Linkis 主要可以分为三大服务板块：</p><ul><li>CGS：计算治理服务组，Computation Governance Services. 完成计算任务和请求的提交、准备、执行、返回结果等主要步骤。</li><li>PES：公共增强服务组，Public Enhancement Services. 主要提供了统一数据源、物料库、上下文等能力。</li><li>MGS：微服务治理服务组，Microservice Governance Services. 该组服务主要复用了 SpringCloud 的能力。</li></ul><h3 id="DataSphereStudio-解决连通集成问题"><a href="#DataSphereStudio-解决连通集成问题" class="headerlink" title="DataSphereStudio 解决连通集成问题"></a>DataSphereStudio 解决连通集成问题</h3><ul><li>串联统一，基于 DSS 工程、权限管理规范，图形化工作流式数据应用开发统一 Ul，AppConn 设计灵活串联不同应用工具系统；</li><li>打通孤岛，基于 Linkis 上下文、物料等公共服务能力；</li><li>快速复用，数据开发工具微模块快速复用能力。</li></ul><p>DataSphereStudio 主要拥有以下引擎支持和框架：</p><p><img src="https://static.pil0txia.com/picgo/image-20230621153913850.webp" alt="image-20230621153913850"></p><h2 id="dss-gitlab-appconn-模块分析"><a href="#dss-gitlab-appconn-模块分析" class="headerlink" title="dss-gitlab-appconn 模块分析"></a>dss-gitlab-appconn 模块分析</h2><h3 id="本地初始化"><a href="#本地初始化" class="headerlink" title="本地初始化"></a>本地初始化</h3><p>当用户访问 DataSphereStudio 的 Scriptis 工作空间时，将会调用 Apache Linkis 的 <code>/filesystem/getUserRootPath</code> 接口，对应 org.apache.linkis.filesystem.restful.api.FsRestfulApi 方法，返回形如 <code>file:///data/linkis/workspace/dss_test01</code> 的 userLocalRootPath。这说明用户在线编写的脚本是储存在服务器本地的。</p><p>要实现以项目为单位的 Git 存储库，就需要在用户创建工作区后，或者首次提交脚本时，将工作区进行 Git 初始化。这可以通过 JGit 库的 <code>FileRepositoryBuilder.create()</code> 方法实现，并提供相应接口。</p><p>如果采用第一种方案，在用户创建工作区后初始化，可以由前端在创建时直接调用该接口，直接对空文件夹初始化，较为快速便捷，缺点在于需要跨模块获取创建的工作区目录；</p><p>如果采用第二种方案，在用户首次提交脚本时初始化，就需要判断用户是否是首次提交。可以检查工作区是否已经初始化为 Git 仓库。如果已经存在<code>.git</code> 文件夹，就无需再进行初始化。这可以通过编写一个私有静态方法实现，缺点在于每次提交时都需要额外的判断，会产生多余的性能开销：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isGitRepository</span><span class="params">(File folder)</span> {</span><br><span class="line">        <span class="type">File</span> <span class="variable">gitFolder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">File</span>(folder, <span class="string">".git"</span>);</span><br><span class="line">        <span class="keyword">return</span> gitFolder.exists() &amp;&amp; gitFolder.isDirectory();</span><br><span class="line">    }</span><br></pre></td></tr></tbody></table></figure><h3 id="远端初始化"><a href="#远端初始化" class="headerlink" title="远端初始化"></a>远端初始化</h3><p>在注册新 DSS 用户时，如果 GitLab 中没有此用户的账号，就自动创建一个，其用户名和密码与 DSS SSO 中保存的当前用户信息一致。</p><p>用户创建工作区时，在该用户的 GitLab 账号中创建与工作区同名的 Git 项目，以此来实现粒度合适的版本管理。</p><p>此外，如果需要允许用户登录 GitLab，以便于进行例如差异比较等更加细化的操作，就需要在 GitLab 侧配置与 DSS SSO 兼容的统一身份认证。</p><h4 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h4><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GitLabManager</span> {</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">GITLAB_API_URL</span> <span class="operator">=</span> <span class="string">"https://gitlab.wedatasphere.com/api/v4"</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">GITLAB_ADMIN_TOKEN</span> <span class="operator">=</span> <span class="string">"MY_GITLAB_ADMIN_TOKEN"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">createGitLabProject</span><span class="params">(String username, String password, String projectName)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 创建用户</span></span><br><span class="line">        createUser(username, password);</span><br><span class="line">        <span class="comment">// 创建项目</span></span><br><span class="line">        createProject(username, projectName);</span><br><span class="line">        <span class="comment">// 添加用户为项目成员</span></span><br><span class="line">        addProjectMember(username, projectName);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">createUser</span><span class="params">(String username, String password)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 构建创建用户的请求 URL</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">createUserUrl</span> <span class="operator">=</span> GITLAB_API_URL + <span class="string">"/users"</span>;</span><br><span class="line">        <span class="comment">// 创建用户的请求体</span></span><br><span class="line">        <span class="type">JSONObject</span> <span class="variable">userRequestBody</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JSONObject</span>();</span><br><span class="line">        userRequestBody.put(<span class="string">"username"</span>, username);</span><br><span class="line">        userRequestBody.put(<span class="string">"password"</span>, password);</span><br><span class="line">        <span class="comment">// 发送创建用户的 POST 请求</span></span><br><span class="line">        sendPostRequest(createUserUrl, userRequestBody.toString(), GITLAB_ADMIN_TOKEN);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">createProject</span><span class="params">(String username, String projectName)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 与 createUser() 方法类似</span></span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">addProjectMember</span><span class="params">(String username, String projectName)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 获取用户 ID</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> getUserId(username);</span><br><span class="line">        <span class="comment">// 获取项目 ID</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">projectId</span> <span class="operator">=</span> getProjectId(projectName);</span><br><span class="line">        <span class="comment">// 构建添加项目成员的请求 URL</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">addMemberUrl</span> <span class="operator">=</span> GITLAB_API_URL + <span class="string">"/projects/"</span> + projectId + <span class="string">"/members"</span>;</span><br><span class="line">        <span class="comment">// 添加项目成员的请求体</span></span><br><span class="line">        <span class="type">JSONObject</span> <span class="variable">memberRequestBody</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JSONObject</span>();</span><br><span class="line">        memberRequestBody.put(<span class="string">"user_id"</span>, userId);</span><br><span class="line">        memberRequestBody.put(<span class="string">"access_level"</span>, <span class="number">30</span>); <span class="comment">// Developer access level</span></span><br><span class="line">        <span class="comment">// 发送添加项目成员的 POST 请求</span></span><br><span class="line">        sendPostRequest(addMemberUrl, memberRequestBody.toString(), GITLAB_ADMIN_TOKEN);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> String <span class="title function_">getUserId</span><span class="params">(String username)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 构建获取用户信息的请求 URL</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">getUserUrl</span> <span class="operator">=</span> GITLAB_API_URL + <span class="string">"/users?username="</span> + username;</span><br><span class="line">        <span class="comment">// 发送获取用户信息的 GET 请求</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">response</span> <span class="operator">=</span> sendGetRequest(getUserUrl, GITLAB_ADMIN_TOKEN);</span><br><span class="line">        <span class="comment">// 解析响应获取用户 ID</span></span><br><span class="line">        <span class="type">JSONArray</span> <span class="variable">users</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JSONArray</span>(response);</span><br><span class="line">        <span class="keyword">if</span> (users.length() &gt; <span class="number">0</span>) {</span><br><span class="line">            <span class="type">JSONObject</span> <span class="variable">user</span> <span class="operator">=</span> users.getJSONObject(<span class="number">0</span>);</span><br><span class="line">            <span class="keyword">return</span> user.getString(<span class="string">"id"</span>);</span><br><span class="line">        }</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> String <span class="title function_">getProjectId</span><span class="params">(String projectName)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="comment">// 与 getUserId() 方法类似</span></span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">sendPostRequest</span><span class="params">(String url, String requestBody, String accessToken)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="type">HttpClient</span> <span class="variable">httpClient</span> <span class="operator">=</span> HttpClients.createDefault();</span><br><span class="line">        <span class="type">HttpPost</span> <span class="variable">httpPost</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpPost</span>(url);</span><br><span class="line">        <span class="comment">// 设置请求头部信息</span></span><br><span class="line">        httpPost.setHeader(HttpHeaders.CONTENT_TYPE, <span class="string">"application/json"</span>);</span><br><span class="line">        httpPost.setHeader(HttpHeaders.ACCEPT, <span class="string">"application/json"</span>);</span><br><span class="line">        httpPost.setHeader(HttpHeaders.AUTHORIZATION, <span class="string">"Bearer "</span> + accessToken);</span><br><span class="line">        <span class="comment">// 设置请求体</span></span><br><span class="line">        <span class="type">StringEntity</span> <span class="variable">stringEntity</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringEntity</span>(requestBody);</span><br><span class="line">        httpPost.setEntity(stringEntity);</span><br><span class="line">        <span class="comment">// 发送请求</span></span><br><span class="line">        <span class="type">HttpResponse</span> <span class="variable">response</span> <span class="operator">=</span> httpClient.execute(httpPost);</span><br><span class="line">        <span class="comment">// 处理响应</span></span><br><span class="line">        <span class="type">HttpEntity</span> <span class="variable">entity</span> <span class="operator">=</span> response.getEntity();</span><br><span class="line">        <span class="type">String</span> <span class="variable">responseString</span> <span class="operator">=</span> EntityUtils.toString(entity);</span><br><span class="line">        <span class="keyword">return</span> responseString;</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">sendGetRequest</span><span class="params">(String url, String accessToken)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">  <span class="comment">// 与 sendPostRequest() 类似</span></span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="功能整合"><a href="#功能整合" class="headerlink" title="功能整合"></a>功能整合</h3><p>在最终的实现中，可以对 Git 操作作出一定整合。例如，在 Intellij IDEA 中，当项目存储库具有可连通的远端时，就只保留了 “推送” 按钮，提交操作将在推送前一并完成。</p><h3 id="Git-操作接口"><a href="#Git-操作接口" class="headerlink" title="Git 操作接口"></a>Git 操作接口</h3><p>此小节给出了 dss-gitlab-appconn 模块中 “提交” 接口的示例代码及注释，配套接口文档可跳转至<a href="#%E7%A4%BA%E4%BE%8B%EF%BC%9A%E6%8F%90%E4%BA%A4%E5%88%B0-GitLab">示例：提交到 GitLab</a></p><h4 id="示例代码-1"><a href="#示例代码-1" class="headerlink" title="示例代码"></a>示例代码</h4><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GitLabConnector</span> {</span><br><span class="line">    <span class="keyword">private</span> Repository repository;</span><br><span class="line">    <span class="keyword">private</span> Git git;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化 GitLab 连接</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">initialize</span><span class="params">(String localPath)</span> <span class="keyword">throws</span> InvalidRemoteException, GitAPIException {</span><br><span class="line">        <span class="comment">// 打开本地存储库</span></span><br><span class="line">        repository = Git.open(<span class="keyword">new</span> <span class="title class_">File</span>(localPath)).getRepository();</span><br><span class="line">        git = <span class="keyword">new</span> <span class="title class_">Git</span>(repository);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 提交到 GitLab</span></span><br><span class="line">    <span class="meta">@PostMapping("/api/commit")</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">commit</span><span class="params">(<span class="meta">@RequestBody</span> CommitRequest commitRequest)</span> <span class="keyword">throws</span> GitAPIException {</span><br><span class="line">        <span class="type">String</span> <span class="variable">authorName</span> <span class="operator">=</span> commitRequest.getAuthorName();</span><br><span class="line">        <span class="type">String</span> <span class="variable">authorEmail</span> <span class="operator">=</span> commitRequest.getAuthorEmail();</span><br><span class="line">        <span class="type">String</span> <span class="variable">commitMessage</span> <span class="operator">=</span> commitRequest.getCommitMessage();</span><br><span class="line">        String[] filePaths = commitRequest.getFilePaths();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 如果未提供 filePaths 可变参数，则默认将所有文件添加到暂存区</span></span><br><span class="line">        <span class="keyword">if</span> (filePaths.length == <span class="number">0</span>) {</span><br><span class="line">            git.add().addFilepattern(<span class="string">"."</span>).call();</span><br><span class="line">        } <span class="keyword">else</span> {</span><br><span class="line">            <span class="type">AddCommand</span> <span class="variable">addCommand</span> <span class="operator">=</span> git.add();</span><br><span class="line">            <span class="keyword">for</span> (String filePath : filePaths) {</span><br><span class="line">                addCommand.addFilepattern(filePath);</span><br><span class="line">            }</span><br><span class="line">            addCommand.call();</span><br><span class="line">        }</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 创建提交命令</span></span><br><span class="line">        <span class="type">CommitCommand</span> <span class="variable">commitCommand</span> <span class="operator">=</span> git.commit();</span><br><span class="line">        <span class="comment">// 设置作者姓名和邮箱</span></span><br><span class="line">        commitCommand.setAuthor(authorName, authorEmail);</span><br><span class="line">        <span class="comment">// 设置提交消息</span></span><br><span class="line">        commitCommand.setMessage(commitMessage);</span><br><span class="line">        <span class="comment">// 执行提交操作</span></span><br><span class="line">        <span class="type">RevCommit</span> <span class="variable">revCommit</span> <span class="operator">=</span> commitCommand.call();</span><br><span class="line">        <span class="comment">// 返回提交的哈希值</span></span><br><span class="line">        <span class="keyword">return</span> revCommit.getName();</span><br><span class="line">    }</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 内部类，用于接收提交请求的 JSON 数据</span></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">CommitRequest</span> {</span><br><span class="line">        <span class="keyword">private</span> String authorName;</span><br><span class="line">        <span class="keyword">private</span> String authorEmail;</span><br><span class="line">        <span class="keyword">private</span> String commitMessage;</span><br><span class="line">        <span class="keyword">private</span> String[] filePaths;</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="实施规范"><a href="#实施规范" class="headerlink" title="实施规范"></a>实施规范</h2><h3 id="开发"><a href="#开发" class="headerlink" title="开发"></a>开发</h3><h4 id="AppConn-三级规范"><a href="#AppConn-三级规范" class="headerlink" title="AppConn 三级规范"></a>AppConn 三级规范</h4><h5 id="一级规范"><a href="#一级规范" class="headerlink" title="一级规范"></a>一级规范</h5><p>SSO 登录规范，如 DolphinScheduler，可以通过左侧菜单跳转到相应页面。</p><h5 id="二级规范"><a href="#二级规范" class="headerlink" title="二级规范"></a>二级规范</h5><p>组织结构框架规范，例如工作空间体系规范，包括角色权限体系框架、角色规范、工程规范等。</p><h5 id="三级规范"><a href="#三级规范" class="headerlink" title="三级规范"></a>三级规范</h5><p>应用开发流程规范。</p><p><img src="https://static.pil0txia.com/picgo/image-20230621161609977.webp" alt="image-20230621161609977"></p><h4 id="组件间调用流程"><a href="#组件间调用流程" class="headerlink" title="组件间调用流程"></a>组件间调用流程</h4><p>以 DolphinScheduler 为例：</p><ul><li>首先会将 DSS 里面的一些 workflow 参数转化成 DolphinScheduler 支持的内部参数</li><li>然后进行节点参数的转换</li><li>再进行功能流的转换</li><li>转换完成后，通过用 HTTP 请求调用 DolphinScheduler 的 update 接口的方式，将已经创建好的 DolphinScheduler 工作流进行更新</li><li>最终即可将 DSS 中的工作流发布到 DolphinScheduler 中</li></ul><p><img src="https://static.pil0txia.com/picgo/image-20230621162958833.webp" alt="image-20230621162958833"></p><p>完成 conversion 工作流转换后，便需要使用 operation 模块，将 DSS 与 DolphinScheduler 的工作流和项目进行关联，并执行增删改查等操作。</p><h4 id="AppConn-开发规范"><a href="#AppConn-开发规范" class="headerlink" title="AppConn 开发规范"></a>AppConn 开发规范</h4><blockquote><p>将会参考：</p><p><a href="https://github.com/WeBankFinTech/DataSphereStudio-Doc/blob/main/zh_CN/%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3/%E7%AC%AC%E4%B8%89%E6%96%B9%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%85%A5DSS%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97.md">第三方系统接入 DSS 开发指南</a></p><p><a href="https://github.com/WeBankFinTech/DataSphereStudio-Doc/blob/main/zh_CN/%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3/AppConn%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97.md">AppConn 开发指南</a></p></blockquote><h4 id="可能的冲突问题"><a href="#可能的冲突问题" class="headerlink" title="可能的冲突问题"></a>可能的冲突问题</h4><p>在用户创建工作区后初始化 Git 存储库时，需要从其它模块获取创建的工作区目录，有可能会导致与其它开发者在此包中的修改产生 Git 冲突。</p><p>为了避免冲突数量过多、过于复杂、难以解决，我将在开始此任务前使用 <code>git rebase</code> 同步主线进度，并尽快完成所有开发。</p><p>在开发阶段性完成时，我将再次使用变基合并。相比于全部整合完成后再使用 <code>merge</code> 合并，这种方式的好处在于单次合并冲突数量少、分支提交记录线性排列较为清晰、联系另一位开发者解决冲突的缓冲时间长、不容易影响工作进度。</p><h4 id="接口可用性问题"><a href="#接口可用性问题" class="headerlink" title="接口可用性问题"></a>接口可用性问题</h4><p>接口在开发完成后可能会产生隐性的问题，尤其是与作用域相关的调用问题。为此，我将利用 IntelliJ IDEA 的 <code>yFiles</code> 图表功能，观察其它接口的各类注解、导入、抽象类和依赖包的引用关系截图，与我的接口的引用关系相比较，确保作用域无误。</p><p>对于接口本身功能是否正常的自测，将使用 Postman 和单元测试配合完成。</p><h4 id="开发-TBD-和-TODO"><a href="#开发-TBD-和-TODO" class="headerlink" title="开发 TBD 和 TODO"></a>开发 TBD 和 TODO</h4><p>在开发时，可能会遇到代码中的 TODO 标记。对此，我将在熟悉需求后，自行建立业务场景，针对场景中的细节开发每一项对应功能，并编写单元测试，确保接口功能正常、可靠。</p><h3 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h3><h4 id="范围"><a href="#范围" class="headerlink" title="范围"></a>范围</h4><p>dss-gitlab-appconn 模块，对象为所有的方法。</p><h4 id="格式"><a href="#格式" class="headerlink" title="格式"></a>格式</h4><p>对于任何一个项目而言，尤其是开源项目，在撰写 JavaDoc 注释时，都需要注意以下方面，以确保注释全面且易于理解：</p><ol><li>摘要（Summary）：提供一个简洁但清晰的摘要，概括该方法或接口的主要功能和作用。</li><li>参数（Parameters）：列出方法或接口接受的所有参数，并为每个参数提供描述。包括参数的名称、类型、是否可为空以及对参数的期望值或用法的说明。</li><li>返回值（Return Value）：描述方法或接口的返回值。指明返回值的类型、可能的返回结果、异常情况或特殊条件等。</li><li>抛出（Throws）：列出方法或接口可能会抛出的异常，并提供每个异常的类型、触发条件和处理建议。</li><li>示例（Examples）：提供一个或多个示例，展示如何使用该方法或接口。可以包括参数设置、方法调用和预期结果的演示。</li><li>注意事项（Notes）：说明任何与方法或接口相关的重要注意事项或限制。</li><li>作者（Author）：标明编写该方法或接口的作者。</li><li>参考（See Also）：指向与该方法或接口相关的其他文档、资源或类。</li><li>版本（Version）：指明该方法或接口首次出现的版本号，并注明修改历史和版本更新。</li><li>修饰符（Modifiers）：指明方法或接口的访问修饰符（例如 public、private、protected）和其他修饰符（例如 static、final）。</li><li>参数范围（Parameter Ranges）：为每个参数提供有效范围或允许的取值范围。</li><li>线程安全性（Thread Safety）：指明该方法或接口的线程安全性信息。例如是否可以在多线程环境中安全地调用。</li><li>依赖关系（Dependencies）：列出方法或接口依赖的其他类、接口或资源。</li></ol><p>具体来说，我将使用 Checkstyle 工具来检查 Java 源代码是否符合代码标准的验证规则，默认情况下，此工具遵守 Google Java Style Guide 和 Sun Code Conventions，需要在 IntelliJ IDEA 中安装 CheckStyle-IDEA 插件配合使用，通过以下方式导入检查样式文件：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Editor -&gt; Code Style -&gt; Java -&gt; Scheme -&gt; Import Scheme -&gt; CheckStyle Configuration</span><br></pre></td></tr></tbody></table></figure><p>在这个代码样式文件中，规定了 Google Java Style Guide 所偏好的 JavaDoc 注释风格，需要：</p><ol><li><p>对齐形参说明</p></li><li><p>对齐抛出异常说明</p></li><li><p>在描述后空行</p></li><li><p>保留无效标签</p></li><li><p>保留空 @param 标签</p></li><li><p>保留空 @return 标签</p></li><li><p>保留空 @throws 标签</p></li><li><p>在右页边距处换行</p></li><li><p>启用前导星号</p></li><li><p>用 @throws 而不是 @exception</p></li><li><p>在空行中生成 <code>&lt;p&gt;</code></p></li><li><p>保留空行</p></li></ol><p>不需要：</p><ol><li>在形参描述后空行</li><li>在 return 后空行</li><li>一行注释不分行</li><li>保留换行</li><li>在新行描述形参</li><li>缩进连续线</li></ol><p>示例如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> sample;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Sample</span> {</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 这是一个方法的描述，如果其长度长到超出右边界，</span></span><br><span class="line"><span class="comment">     * &lt;p&gt;</span></span><br><span class="line"><span class="comment">     * 就需要另起一行，在新的段落继续描述。</span></span><br><span class="line"><span class="comment">     * &lt;p/&gt;</span></span><br><span class="line"><span class="comment">     * 可以手动换行。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> i                  简短命名的参数描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> longParameterName  长命名的参数描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> missingDescription 缺少描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 返回描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> XXXException 异常描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> YException   异常描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> ZException</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@invalidTag</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod</span><span class="params">(<span class="type">int</span> i,</span></span><br><span class="line"><span class="params">                                        <span class="type">int</span> longParameterName,</span></span><br><span class="line"><span class="params">                                        <span class="type">int</span> missingDescription)</span></span><br><span class="line">            <span class="keyword">throws</span> XXXException, YException, ZException;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 单行注释</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod2</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 简单方法描述</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod3</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h4 id="提交形式"><a href="#提交形式" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个功能所包含的文件与类为单位，在 WeBankFinTech/DataSphereStudio 仓库新建一个 Issue，声明正在为哪个模块的哪个功能撰写注释，然后向 Pil0tXia/DataSphereStudio 仓库的 pil0txia_doc_{ISSUE ID} 分支提交 Git Commit。当一个主要功能的全部接口和方法的 JavaDoc 注释均已撰写完成时，从该分支向 WeBankFinTech/DataSphereStudio 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 WeBankFinTech/DataSphereStudio 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成一个 Contributor 与 Reviewer 异步的贡献形式。</p><h3 id="文档"><a href="#文档" class="headerlink" title="文档"></a>文档</h3><h4 id="范围-1"><a href="#范围-1" class="headerlink" title="范围"></a>范围</h4><p>对于课题预期任务而言，需要编写文档的范围 dss-gitlab-appconn 模块，对象为所有的接口。</p><h4 id="格式-1"><a href="#格式-1" class="headerlink" title="格式"></a>格式</h4><p>在正式创建 md 文件之前，需要先思考接口文档在侧边栏目录中的位置和组织形式，并且在仓库中新建属于接口文档的目录。</p><p>DataSphereStudio-Doc 支持英文与中文两种语言，这两种语言的 Markdown 文件是分开存放的，分别位于 <code>en_US</code> 和 <code>zh_CN</code> 目录。两者目录层级是一样的，是为了支持多语言的文档展示。</p><p>在编写文档时，需要注意以下方面，以确保 Markdown 语法可以被正确地解析，并支持多种 Markdown 渲染器的排版：</p><ol><li>目录结构：根据接口的层级结构或逻辑关系，创建一个清晰的目录结构。使用标题和子标题来组织接口文档，且标题层级不超过四级。</li><li>接口概述：对每个接口提供一个简要概述，描述其用途、输入和输出等关键信息。指明接口的名称、路径和 HTTP 方法。</li><li>参数说明：列出每个接口所需的参数，并提供参数的名称、类型、是否必需、取值范围以及示例值等信息。对于复杂的参数结构，可以使用表格或嵌套列表来清晰展示参数的层级关系和说明。</li><li>响应示例：提供一个或多个示例，展示接口的调用和返回结果。示例可以包括请求和响应的数据结构、状态码和消息等信息。对于可选的响应字段，也可以提供示例值。</li><li>异常处理：描述可能的错误情况和异常，以及相应的错误码和错误消息。提供每个异常的名称、描述和处理建议。</li><li>接口详情：为每个接口提供更详细的说明，包括接口的功能、用法、限制、注意事项和最佳实践等。可以使用段落、列表和代码块来组织和展示信息。</li><li>参考资料：提供与该模块或接口相关的其他文档、资源或链接。</li><li>更新记录：在文档中提供更新记录和重要变更，指明版本号、修改内容和日期。</li><li>示例代码：为关键接口或复杂场景提供示例代码。</li><li>格式和排版：使用代码块和强调样式等来保持一致的格式和排版。</li><li>图表和图像：可以使用图表、图像或流程图等可视化工具来说明接口的工作流程或数据流动。</li><li>文档导航：在官网上发布时，需要在整个网站上提供简单且直观的目录导航，使访问者能够轻松找到和浏览 dss-gitlab-appconn 模块的接口文档。</li></ol><p>在编写 Markdown 文档之前，我应该已经在接口的代码中撰写了注释，以便从代码中对照文档。</p><h4 id="提交形式-1"><a href="#提交形式-1" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个接口为单位，在 WeBankFinTech/DataSphereStudio-Doc 仓库新建一个 Issue，声明正在为哪个接口撰写注释，然后向 Pil0tXia/DataSphereStudio-Doc 仓库的 pil0txia_docs_{ISSUE ID} 分支提交 Git Commit。此处的分支名称与撰写注释任务的分支名称并不相同，发布于 Web 网站上的使用文档的英文通常使用 docs 表示，与承载撰写注释任务的 doc 分支作区分。</p><p>当一个主要功能的全部接口的 Markdown 文档均已撰写完成时，从该分支向 WeBankFinTech/DataSphereStudio-Doc 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 WeBankFinTech/DataSphereStudio-Doc 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成一个 Contributor 与 Reviewer 异步的贡献形式。</p><h4 id="示例：提交到-GitLab"><a href="#示例：提交到-GitLab" class="headerlink" title="示例：提交到 GitLab"></a>示例：提交到 GitLab</h4><h5 id="接口说明"><a href="#接口说明" class="headerlink" title="接口说明"></a>接口说明</h5><p>提交功能的接口用于将指定的文件或所有文件添加到 GitLab 的暂存区，并创建一个新的提交。</p><h5 id="请求地址"><a href="#请求地址" class="headerlink" title="请求地址"></a>请求地址</h5><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">POST https://dss-open.wedatasphere.com/api/rest_j/v1/gitlab/commit</span><br></pre></td></tr></tbody></table></figure><h5 id="请求参数"><a href="#请求参数" class="headerlink" title="请求参数"></a>请求参数</h5><table><thead><tr><th>参数名</th><th>类型</th><th>是否必需</th><th>描述</th></tr></thead><tbody><tr><td> authorName</td><td> 字符串</td><td>是</td><td>提交的作者姓名</td></tr><tr><td> authorEmail</td><td> 字符串</td><td>是</td><td>提交的作者邮箱</td></tr><tr><td> commitMessage</td><td> 字符串</td><td>是</td><td>提交的消息</td></tr><tr><td> filePaths</td><td> 字符串数组</td><td>否</td><td>要添加到暂存区的文件路径列表。如果不提供该参数，则默认添加所有文件到暂存区。</td></tr></tbody></table><h5 id="请求示例"><a href="#请求示例" class="headerlink" title="请求示例"></a>请求示例</h5><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"authorName"</span><span class="punctuation">:</span> <span class="string">"Xia Tian"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"authorEmail"</span><span class="punctuation">:</span> <span class="string">"admin@pil0txia.com"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"commitMessage"</span><span class="punctuation">:</span> <span class="string">"[Feature]: xxxxxx"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"filePaths"</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">"file1.txt"</span><span class="punctuation">,</span> <span class="string">"file2.txt"</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><h5 id="返回参数"><a href="#返回参数" class="headerlink" title="返回参数"></a>返回参数</h5><table><thead><tr><th>参数名</th><th>类型</th><th>描述</th></tr></thead><tbody><tr><td> commitHash</td><td> 字符串</td><td>提交的哈希值</td></tr></tbody></table><h5 id="返回结果示例"><a href="#返回结果示例" class="headerlink" title="返回结果示例"></a>返回结果示例</h5><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"commitHash"</span><span class="punctuation">:</span> <span class="string">"6d5e26a3d8a79242d2018f39b61ae4978b6b7c83"</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><h5 id="返回码说明"><a href="#返回码说明" class="headerlink" title="返回码说明"></a>返回码说明</h5><table><thead><tr><th>返回码</th><th>说明</th></tr></thead><tbody><tr><td> 200</td><td> 请求成功</td></tr><tr><td> 400</td><td> 请求参数缺失或格式错误</td></tr><tr><td> 500</td><td> 服务器内部错误</td></tr></tbody></table><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><h4 id="范围-2"><a href="#范围-2" class="headerlink" title="范围"></a>范围</h4><p>dss-gitlab-appconn 模块，对象为所有的方法。</p><h4 id="要求"><a href="#要求" class="headerlink" title="要求"></a>要求</h4><p>测试代码的编写可以参考现有的测试文件。在单元测试时，需要注意以下方面：</p><ol><li>输入验证：确保测试覆盖了各种可能的输入情况，包括边界值、无效值、空值和有效值。</li><li>接口状态：测试应该涵盖接口的各种状态和路径。例如，在测试 <code>testTopicCreateRequestSetName</code> 时，应该包括设置名称为 <code>null</code> 的情况以验证接口的响应。</li><li>异常情况：测试接口在异常情况下的行为。例如接口是否正确地处理了错误的输入或不正确的参数。</li><li>序列化和反序列化：对于涉及对象的序列化和反序列化的接口，应该编写测试来验证对象的正确序列化和反序列化。确保序列化后的数据包含所需的字段，并且在反序列化后可以正确还原为对象。</li><li>副作用和一致性：如果接口执行了一些副作用或对系统状态进行了更改，测试应该验证这些副作用是否按预期发生，并确保接口的行为一致。</li><li>测试覆盖率：尽量覆盖接口的各个路径和代码分支，以确保测试足够全面。使用代码覆盖工具（如 JaCoCo 和 Codecov 等）来评估测试的覆盖率，并尽量达到较高的覆盖率。</li><li>并发和性能：如果接口设计要求支持高并发或高性能，需要验证接口在高并发和高负载情况下的表现和性能。</li><li>引入依赖：在测试中正确模拟或提供依赖项。</li><li>可读性和可维护性：编写清晰、简洁、可读性强的测试代码，使用有意义的命名和注释，以便他人能够理解和维护测试。</li><li>持续集成和自动化：将测试集成到持续集成（CI）流程中，在每次代码提交时自动运行测试。</li></ol><h4 id="提交形式-2"><a href="#提交形式-2" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个功能所包含的文件与类为单位，在 WeBankFinTech/DataSphereStudio 仓库新建一个 Issue，声明正在为哪个模块的哪个功能编写测试代码，然后向 Pil0tXia/DataSphereStudio 仓库的 pil0txia_test_{ISSUE ID} 分支提交 Git Commit。当一个主要功能的全部接口和方法的 JavaDoc 注释均已撰写完成时，从该分支向 WeBankFinTech/DataSphereStudio 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 WeBankFinTech/DataSphereStudio 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成 Contributor 与 Reviewer 异步的贡献形式。</p><h2 id="时间规划"><a href="#时间规划" class="headerlink" title="时间规划"></a>时间规划</h2><h3 id="每周时间安排"><a href="#每周时间安排" class="headerlink" title="每周时间安排"></a>每周时间安排</h3><p>每周约 32 小时：</p><ul><li>周一至周五，每日 3 小时</li><li>周末，每日 8 小时</li><li>向导师汇报开发进度与安排，1 小时</li></ul><h3 id="项目进度"><a href="#项目进度" class="headerlink" title="项目进度"></a>项目进度</h3><table><thead><tr><th align="center">任务</th><th align="center">时间</th></tr></thead><tbody><tr><td align="center">熟悉项目</td><td align="center"> 7.1 - 7.7</td></tr><tr><td align="center"> 熟悉 AppConn 对接规范</td><td align="center"> 7.8 - 7.14</td></tr><tr><td align="center"> 开发 Git 基础操作 API</td><td align="center">7.15 - 7.21</td></tr><tr><td align="center"> 配置 GitLab 第三方系统</td><td align="center"> 7.21 - 8.4</td></tr><tr><td align="center"> 撰写中期报告</td><td align="center"> 8.5 - 8.11</td></tr><tr><td align="center"> 将 GitLab 与 API 整合为插件</td><td align="center"> 8.12 - 8.25</td></tr><tr><td align="center"> 测试接口功能</td><td align="center"> 8.25 - 8.31</td></tr><tr><td align="center"> 编写 JavaDoc 注释</td><td align="center"> 9.1 - 9.7</td></tr><tr><td align="center"> 编写接口文档</td><td align="center"> 9.8 - 9.14</td></tr><tr><td align="center"> 开发 TBD 和 TODO</td><td align="center">9.15 - 9.21</td></tr><tr><td align="center"> 撰写结题报告</td><td align="center"> 9.22 - 10.5</td></tr><tr><td align="center"> 弹性时间安排</td><td align="center"> 10.5 - 10.11</td></tr></tbody></table><h2 id="个人简介"><a href="#个人简介" class="headerlink" title="个人简介"></a>个人简介</h2><p>我是来自南京信息工程大学的夏天，大三，目前正在联想实习，承担 Spring Cloud + Kafka + Eureka 方面的后端开发工作。这是我的<a href="https://www.pil0txia.com/">博客</a>、<a href="https://docs.pil0txia.com/">文档</a>和 <a href="https://github.com/Pil0tXia">Github</a>，日均 PV 400 左右，有些文章的谷歌 / 必应排名也比较高。</p><p>每每使用开源工具和框架，都很感谢开发者的付出。在我注册 Github 账号的第五年，我意识到自己应该真正地去研究透彻一个框架、参与一个社区、进行贡献，我也非常希望自己能在时间还算充裕的学生时代，多尝试一些新技术，抓住这个契机。</p><p>衷心希望能参加张旗导师您指导的 GLCC 课题。</p><p>联系方式：<a href="mailto:admin@pil0txia.com">admin@pil0txia.com</a></p><h2 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h2><p>在已规划的 DSS 1.2.0 中，包含以下新功能：</p><ul><li>数仓规划：包含主题域、数仓分层、修饰词等</li><li>数据模型中心：包括指标、维度、度量和向导式建表等</li><li>数据资产中心：打通 Apache Altas，提供数据血缘能力</li></ul><p>在后续的社区贡献中，我会深入理解产品定位，设想产品场景，主动发现增长点与增强点，并持之以恒地作出贡献。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;6-27 更新：已中选，虽然最多可以申请两个课题，但很遗憾 GLCC 规定学生只能参与第一个课题。导师对我很好，会继续关注 WeDataSphere 社区的！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://static.pil0txia.com</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="GLCC" scheme="https://www.pil0txia.com/tags/GLCC/"/>
    
    <category term="Proposal" scheme="https://www.pil0txia.com/tags/Proposal/"/>
    
    <category term="DataSphereStudio" scheme="https://www.pil0txia.com/tags/DataSphereStudio/"/>
    
    <category term="GitLab" scheme="https://www.pil0txia.com/tags/GitLab/"/>
    
  </entry>
  
  <entry>
    <title>【开源之夏】Apache EventMesh 整合 admin 模块课题 Proposal（已中选）</title>
    <link href="https://www.pil0txia.com/post/2023-06-23_glcc-2023-eventmesh/"/>
    <id>https://www.pil0txia.com/post/2023-06-23_glcc-2023-eventmesh/</id>
    <published>2023-06-23T13:41:24.000Z</published>
    <updated>2023-10-11T16:32:24.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>6-27 更新：已中选，能从海内外名校候选者中脱颖而出是一件很荣幸的事情，感谢导师的信任，Apache 邮箱我来了！</p><p><img src="https://static.pil0txia.com/picgo/image-20230627234414550.webp"></p></blockquote><h1 id="项目申请书"><a href="#项目申请书" class="headerlink" title="项目申请书"></a>项目申请书</h1><h2 id="课题名称"><a href="#课题名称" class="headerlink" title="课题名称"></a>课题名称</h2><p><a href="https://www.gitlink.org.cn/glcc/2023/subjects/detail/580">eventmesh admin 模块优化完善</a></p><p>申请人：夏天</p><p>导师：薛炜明 | <a href="mailto:mikexue@apache.org">mikexue@apache.org</a></p><h2 id="社区简介"><a href="#社区简介" class="headerlink" title="社区简介"></a>社区简介</h2><p>Apache EventMesh 是一个用于解耦应用和后端中间件层的动态云原生事件驱动架构基础设施。它支持广泛的用例，包括复杂的混合云、使用了不同技术栈的分布式架构。</p><ul><li>轻松构建松耦合、分布式事件驱动架构</li><li>开放兼容，原生支持 CloudEvents SDK 和 API，易扩展</li><li>打破数据孤岛，支持企业全联接、数字化、多云协同</li></ul><h2 id="题目简介"><a href="#题目简介" class="headerlink" title="题目简介"></a>题目简介</h2><p>eventmesh admin 模块为 eventmesh 项目的管理模块，目前 eventmesh admin 模块包含了不同协议的查询接口、客户端操作接口、webhook 配置操作接口，以及对事件存储的操作等，现有接口需补充相关注释，并测试是否功能正常，同时输出 admin 模块对应的文档与功能展示，针对事件存储的操作需进行扩展补充更多功能。</p><h2 id="编码任务"><a href="#编码任务" class="headerlink" title="编码任务"></a>编码任务</h2><ul><li>采用 checkStyle.xml 代码风格配置中的格式，为现有的 admin 模块接口编写 JavaDoc 注释</li><li>在官方 Docusaurus 文档仓库中编写 admin 模块的接口文档</li><li>测试现有接口功能并完善优化</li><li>现有的 admin 模块对事件存储的管理接口耦合在 runtime 运行时中，且支持的管理范围也局限在单机。为了让 admin 模块具有集群管理能力，需要将 org.apache.eventmesh.runtime.admin 包中的所有管理接口提取到 eventmesh-admin 模块，以便于支持单独部署</li><li>为 admin 管理中心增加一键启停等管理命令，支持向集群节点发送控制命令</li><li>更新 eventmesh-runtime/scripts 中的管理脚本</li><li>完成 admin 模块中 TBD（待决定）和 TODO（待开发）的功能</li></ul><p>如果上述任务完成进度快于预期，可以在活动结束前和后续社区贡献中继续完成以下任务：</p><ul><li>admin 模块与 eventmesh-registry-plugin 注册中心交互，runtime 把信息暴露给 registry，用 registry 的 API，将对注册中心的调用整合到 admin 管理中心</li><li> Data Mesh: 支持在不同的事件存储之间传输数据，使用 EventMesh SDK 打通各事件存储</li><li>完善事件存储接口，增加支持的事件存储</li><li>开发一个支持统一各事件存储的通用连接器，减少连接器插件的使用方法更新时业务侧的代码变动</li><li>支持 MQTT 协议</li></ul><h2 id="核心模块分析"><a href="#核心模块分析" class="headerlink" title="核心模块分析"></a>核心模块分析</h2><h3 id="解耦架构"><a href="#解耦架构" class="headerlink" title="解耦架构"></a>解耦架构</h3><p>Apache EventMesh 可以将应用中的业务逻辑与基础设施的强绑定解耦，其解耦原理与另一开源项目 Apache Linkis 类似，通过 eventmesh-storage-plugin 模块调用 eventmesh-connectors 模块中的 sink connector 和 source connector，以插件形式提供对不同事件存储的支持能力。</p><p><img src="https://static.pil0txia.com/picgo/image-20230613214715150.webp" alt="image-20230613214715150"></p><p><img src="https://static.pil0txia.com/picgo/image-20230613214912291.webp" alt="image-20230613214912291"></p><p>插件化的方式最主要的优点在于方便，可以让所有的使用者根据具体的基础设施方案去使用不同的插件。eventmesh-runtime 是一种微内核的设计，只需要给 runtime 设计好它的运行态，其它的功能都从 API 去使用。定义好不同的 API 之后，就可以进一步定义不同的插件了，比如注册中心模块、存储模块等。</p><p>eventmesh-connectors 模块是三月份近期新提取的，在此之前，有一个名为 eventmesh-connector-plugin 的模块，这两者的功能是不完全相同的。eventmesh-connector-plugin 模块转型为了目前的 eventmesh-storage-plugin 模块，原先的 eventmesh-connector-plugin 的角色是 EventMesh 的事件存储，现在的 eventmesh-connectors 模块是更纯粹的连接器，承担数据同步。也就是说，source 和 sink 两端进行了解耦处理，比如 source 端是 RocketMQ，sink 端可以是其他的业务，但在这两端中间会经过 eventmesh-storage-plugin 模块处理。</p><h3 id="SPI-反射机制"><a href="#SPI-反射机制" class="headerlink" title="SPI 反射机制"></a>SPI 反射机制</h3><p>通过 org.apache.eventmesh.starter.StartUp 的 main () 方法启动 eventmesh-runtime 后，请求的处理是利用 SPI 反射机制，外加调用其它模块的 API 来实现的。如果需要支持某种事件存储，除了在代码层面强依赖的方式，更好的方式便是将插件的 jar 包加载后使用 SPI 的反射机制来将该插件实例化，这一点与 Apache Dubbo 的 SPI 机制是比较类似的。</p><p>以 eventmesh-storage-rocketmq 事件存储插件为例，通过在定义接口时添加 @EventMeshSPI 注解，并定义其单例模式与插件类型，可以在 classpath 加载这个类的时候，识别注解并寻找对应的实现类。在实现类关联的文件中定义了 RocketMQ 的别名，也就是实现类的全路径：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rocketmq=org.apache.eventmesh.storage.rocketmq.producer.RocketMQProducerImpl</span><br></pre></td></tr></tbody></table></figure><p>于是，加载存储层的 API，例如 org.apache.eventmesh.api.producer.Producer 接口时，便可以映射该接口有一个 RocketMQ 的实现，且找到该实现对应的实现类，再通过对实现类进行反射加载，就可以将其实例化。</p><h3 id="消息的处理流程"><a href="#消息的处理流程" class="headerlink" title="消息的处理流程"></a>消息的处理流程</h3><p>Apache EventMesh 目前支持接收 TCP、HTTP、gRPC 三种协议的请求。eventmesh-runtime 相当于 Apache EventMesh 的服务端，eventmesh-example 使用了 SDK，就相当于客户端。</p><p>以 TCP 为例，在收到业务侧发送的请求后，将首先由 Server 层的 org.apache.eventmesh.runtime.boot.EventMeshTCPServer 处理。这是一个基于 Netty 框架编写的 TCP 服务器，Netty 是一个异步事件驱动的网络应用框架，用于快速开发可维护的高性能协议服务器和客户端。它的 API 易用、性能高且社区活跃，在 Apache Dubbo、RocketMQ 等项目中都有使用。</p><p>EventMeshTCPServer 的入口在 startServer () 方法，在这个方法中通过管道的方式一步步添加对 TCP 数据包的处理。在经过 Handler 处理后，数据包进入 EventMeshTcpMessageDispatcher 分发器，使用 TCP 命令字区分数据包类型，并以异步回调的方式将数据包分发到对应任务的线程池中进行处理。从客户端的角度来讲，关键字对应了握手、心跳、订阅、取消订阅、开始监听消息、发送异步事件、发送广播事件等多种场景，完整信息可以参考 TCP 文档。</p><p>在异步回调中，EventMesh 作为发送方，在向基础设施的事件存储发送请求后，不会同步等待请求的执行结果，会继续发送其它请求，或者去执行其它任务。具体来说，虽然发送方可以 “发后不管”，但它需要一个网络 IO 线程对接收方发回的处理结果保持监听状态。执行业务逻辑的线程将请求的上下文托管给网络 IO 线程后，便可以继续处理业务逻辑，等到网络 IO 线程收到响应时，再交给业务逻辑线程。两者是错开运行的，虽然请求的延迟在上下文切换上会产生一定的性能损耗，但这种异步模式对吞吐量和高可用性的提升是更为可观的。</p><p>在异步的基础上，EventMesh 对事件存储有很多 Callback 回调的机制。以 RocketMQ 为例，EventMesh 用 RocketMQ 插件向基础设施发送请求时，EventMesh 的业务逻辑线程本身在网络 IO 线程发送完毕之后就已经离开了，等到收到基础设施中的 RocketMQ 服务端的响应之后，再去调 RocketMQ 客户端的回调，这个回调会再回到 EventMesh 里。回调机制整体是一个递归的流程，一层一层往深处走，然后一层一层往外返回，得出最终的结果。</p><p>具体来说，在 EventMeshTcpMessageDispatcher.channelRead0 () 中使用 dispatch () 方法根据 TCP 命令字分发数据包时，会使用 MessageTransferTask () 方法处理消息的收发任务，先把 TCP 协议转换为 EventMesh 内部通用的 CloudEvent 协议，经过 Traceback 和 TCP 限流等处理后，调用 createSendCallback () 方法，开始递归。递归返回的响应经过 AbstractTask 类抽象的 upstreamMsg () 方法包装，此时 upstreamMsg () 就做好了发送给事件存储基础设施的准备。再以 SessionSender 的 send () 方法为起点，通过 API 层 org.apache.eventmesh.api.producer.Producer 类中的 publish () 方法将消息异步地广播出去，使订阅者收到消息。执行结果保存在 sendStatus 中。</p><p>其中，createSendCallback () 方法是最为核心的，它创建了一个回调任务，并使用 SendCallback () 方法，在 onSuccess 时，将消息的内容、开始时间、执行时间、上下文等数据通过 writeAndFlush () 方法写给发送方。</p><p>什么时候调用 onSuccess () 方法是最重要的，它是在存储层被调用的，如 eventmesh-storage-kafka 等。此时已经使用 Kafka 客户端 KafkaProducer.send () 的 doSend () 方法向基础设施发送了 Kafka 形式的回调命令，并通过 onCompletion () 返回结果。</p><p>publish () 方法的每一种实现都对应着一个支持特定事件存储的实现类，其具体采用哪种实现就是利用了 SPI 机制加载。每种实现都有一些差别，例如，在 RocketMQ 的实现中，就在 sendAsync () 方法中使用了 RocketMQMessageFactory.createWriter () 方法和 sendCallbackConvert () 方法将 CloudEvent 和 EventMesh 自定义的 SendCallback 回调类型转换为 RocketMQ 支持的 org.apache.rocketmq.common.message.Message 消息类型和 org.apache.rocketmq.client.producer.SendCallBack 回调类型，而在 Kafka 的实现中，回调函数类型的转换是通过匿名函数 <code>(metadata, exception) -&gt; { ... }</code> 实现的。</p><p>总而言之，从整体的、消息的角度看，一个请求的流程，是先由 EventMesh 客户端发给 EventMesh，EventMesh 再用 RocketMQ 客户端发给 RocketMQ，并且传一个 SendCallback 回调类型给 RocketMQ，并要求 RocketMQ 在请求处理结束、调用 RocketMQ 的 onSuccess () 方法时，使用 EventMesh 的 onSuccess () 方法返回回调结果。</p><p>从 EventMesh 内部层次的角度看，消息一开始会进入 Server 层，然后利用 SPI 反射机制，在 API 层利用异步回调去调用存储层的事件存储插件，一方面向事件存储发送消息和回调，另一方面向 API 层回调，等到事件存储返回执行的结果之后，将结果等信息返回给 API，然后再由 API 把执行结果原路返回调用方。</p><h2 id="admin-模块分析"><a href="#admin-模块分析" class="headerlink" title="admin 模块分析"></a>admin 模块分析</h2><h3 id="模块现状"><a href="#模块现状" class="headerlink" title="模块现状"></a>模块现状</h3><p>课题任务所指的 admin 模块包含了 eventmesh-admin 模块和 org.apache.eventmesh.runtime.admin 包两大部分，不同协议的查询接口、客户端操作接口、webhook 配置操作接口，以及对事件存储的操作等目前都包含在后者中，前者目前只提供了 RocketMQ 事件存储的管理功能，并不能管理 EventMesh 节点，且没有提供 SPI 反射机制的插件加载能力。</p><h3 id="接口功能"><a href="#接口功能" class="headerlink" title="接口功能"></a>接口功能</h3><p>此小节列举并分析了 org.apache.eventmesh.runtime.admin 包中所有接口的功能，详细接口文档示例可跳转<a href="#%E7%A4%BA%E4%BE%8B%EF%BC%9A%E8%8E%B7%E5%8F%96%E7%B3%BB%E7%BB%9F%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF">示例：获取系统配置信息</a></p><h4 id="configuration"><a href="#configuration" class="headerlink" title="/configuration"></a>/configuration</h4><p>位于 <code>ConfigurationHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/</code>。</p><p>用于获取当前 EventMesh 节点的基本配置信息，包括服务名称、服务环境和各协议监听端口地址等。</p><p>接口中定义了三个方法：</p><ol><li><code>preflight(HttpExchange httpExchange)</code> 方法处理 OPTIONS 请求，用于在实际请求之前发送 CORS（跨源资源共享）响应头，允许跨域请求访问。它将响应头添加到 <code>httpExchange</code> 对象中，并发送 200 状态码。</li><li><code>get(HttpExchange httpExchange)</code> 方法处理 GET 请求，用于获取配置信息。它将配置信息组装成 <code>GetConfigurationResponse</code> 对象，并将其序列化为 JSON 字符串，并将其作为响应的内容返回给客户端。</li><li><code>handle(HttpExchange httpExchange)</code> 方法是接口的主要处理方法，根据请求的方法类型（OPTIONS 或 GET）分别调用上述两个方法进行处理。</li></ol><p>其中，<code>handle()</code> 方法是由 com.sun.net.httpserver.HttpHandler 实现的，如图所示：</p><p><img src="https://static.pil0txia.com/picgo/image-20230617152925838.webp" alt="image-20230617152925838"></p><h4 id="client"><a href="#client" class="headerlink" title="/client"></a>/client</h4><p>此接口用于连接 EventMesh 节点，但代码中没有此路径的请求映射，需要修复。对应 eventmesh-dashboard 按钮 <code>/#Save</code>。</p><h4 id="metrics"><a href="#metrics" class="headerlink" title="/metrics"></a>/metrics</h4><p>位于 <code>MetricsHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/metrics</code>。</p><p>用于获取指标数据的汇总信息，包含以下指标：</p><p>HTTP 指标：</p><ul><li><code>maxHTTPTPS()</code>: 最大 HTTP 事务数 / 秒</li><li><code>avgHTTPTPS()</code>: 平均 HTTP 事务数 / 秒</li><li><code>maxHTTPCost()</code>: 最大 HTTP 请求耗时</li><li><code>avgHTTPCost()</code>: 平均 HTTP 请求耗时</li><li><code>avgHTTPBodyDecodeCost()</code>: 平均 HTTP 请求体解析耗时</li><li><code>getHttpDiscard()</code>: HTTP 请求丢弃数</li><li><code>maxSendBatchMsgTPS()</code>: 最大批量发送消息数 / 秒</li><li><code>avgSendBatchMsgTPS()</code>: 平均批量发送消息数 / 秒</li><li><code>getSendBatchMsgNumSum()</code>: 发送批量消息总数</li><li><code>getSendBatchMsgFailNumSum()</code>: 发送批量消息失败总数</li><li><code>getSendBatchMsgFailRate()</code>: 发送批量消息失败率</li><li><code>getSendBatchMsgDiscardNumSum()</code>: 发送批量消息丢弃总数</li><li><code>maxSendMsgTPS()</code>: 最大单条消息发送数 / 秒</li><li><code>avgSendMsgTPS()</code>: 平均单条消息发送数 / 秒</li><li><code>getSendMsgNumSum()</code>: 发送单条消息总数</li><li><code>getSendMsgFailNumSum()</code>: 发送单条消息失败总数</li><li><code>getSendMsgFailRate()</code>: 发送单条消息失败率</li><li><code>getReplyMsgNumSum()</code>: 回复消息总数</li><li><code>getReplyMsgFailNumSum()</code>: 回复消息失败总数</li><li><code>maxPushMsgTPS()</code>: 最大推送消息数 / 秒</li><li><code>avgPushMsgTPS()</code>: 平均推送消息数 / 秒</li><li><code>getHttpPushMsgNumSum()</code>: HTTP 推送消息总数</li><li><code>getHttpPushFailNumSum()</code>: HTTP 推送消息失败总数</li><li><code>getHttpPushMsgFailRate()</code>: HTTP 推送消息失败率</li><li><code>maxHTTPPushLatency()</code>: 最大 HTTP 推送延迟</li><li><code>avgHTTPPushLatency()</code>: 平均 HTTP 推送延迟</li><li><code>getBatchMsgQueueSize()</code>: 批量消息队列大小</li><li><code>getSendMsgQueueSize()</code>: 发送消息队列大小</li><li><code>getPushMsgQueueSize()</code>: 推送消息队列大小</li><li><code>getHttpRetryQueueSize()</code>: HTTP 重试队列大小</li><li><code>avgBatchSendMsgCost()</code>: 平均批量发送消息耗时</li><li><code>avgSendMsgCost()</code>: 平均单条消息发送耗时</li><li><code>avgReplyMsgCost()</code>: 平均回复消息耗时</li></ul><p>TCP 指标：</p><ul><li><code>getRetrySize()</code>: 重试队列大小</li><li><code>getClient2eventMeshTPS()</code>: 客户端到 EventMesh 的消息数 / 秒</li><li><code>getEventMesh2mqTPS()</code>: EventMesh 到 MQ 的消息数 / 秒</li><li><code>getMq2eventMeshTPS()</code>: MQ 到 EventMesh 的消息数 / 秒</li><li><code>getEventMesh2clientTPS()</code>: EventMesh 到客户端的消息数 / 秒</li><li><code>getAllTPS()</code>: 所有消息数 / 秒</li><li><code>getAllConnections()</code>: 所有连接数</li><li><code>getSubTopicNum()</code>: 订阅主题数</li></ul><p>接口中定义了三个方法：</p><ol><li><code>preflight()</code> 方法处理 OPTIONS 请求，与 <code>/configuration</code> 接口类似，以下接口将不再赘述。</li><li><code>get(HttpExchange httpExchange)</code> 方法处理 GET 请求，用于获取指标数据的汇总信息。该方法将 HTTP 和 TCP 的汇总指标数据封装进 <code>GetMetricsResponse</code> 对象，并将其序列化为 JSON 字符串，然后作为响应体返回。</li><li><code>handle()</code> 方法是接口的主要处理方法，与 <code>/configuration</code> 接口类似，以下接口将不再赘述。</li></ol><h4 id="registry"><a href="#registry" class="headerlink" title="/registry"></a>/registry</h4><p>位于 <code>MetricsHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/registry</code>。</p><p>用于获取 EventMesh 集群列表。</p><p>主要功能为从 Registry 对象中获取 EventMesh 集群信息，然后封装成 GetRegistryResponse 对象列表，并按照 EventMeshClusterName 进行排序。</p><h4 id="topic"><a href="#topic" class="headerlink" title="/topic"></a>/topic</h4><p>位于 <code>TopicHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/topic</code> 及 <code>Create Topic</code> 和 <code>Remove</code> 按钮。</p><p>用于处理对主题的管理操作，包括获取主题列表 (GET)、创建主题 (POST) 和删除主题 (DELETE)。</p><h4 id="event"><a href="#event" class="headerlink" title="/event"></a>/event</h4><p>位于 <code>EventHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/event</code> 及下拉框和 <code>Create Event</code> 按钮。</p><p>用于处理对事件的管理操作，包括获取事件列表 (GET) 和创建事件 (POST)。</p><p>其中 GET 方法支持以 <code>topicName</code> 为查询参数，并使用 <code>offset</code> 和 <code>length</code> 参数分页查询。</p><h4 id="workflow"><a href="#workflow" class="headerlink" title="/workflow"></a>/workflow</h4><p>此接口用于获取、新增或删除工作流，但代码中没有此路径的请求映射，需要修复。对应 eventmesh-dashboard 路径 <code>/workflow</code>。</p><h4 id="undefined-catalog"><a href="#undefined-catalog" class="headerlink" title="/undefined/catalog"></a>/undefined/catalog</h4><p>此接口用于获取、新增或删除活动目录，但代码中没有此路径的请求映射，需要修复。对应 eventmesh-dashboard 路径 <code>/eventCatalogs</code>。</p><h4 id="client-tcp"><a href="#client-tcp" class="headerlink" title="/client/tcp"></a>/client/tcp</h4><p>位于 <code>TCPClientHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/tcp</code>。</p><p>用于处理与 TCP 客户端相关的管理操作，包括获取已连接的 TCP 客户端列表和删除 TCP 客户端（断开指定客户端的连接）。</p><p><code>list()</code> 方法：处理 GET 请求</p><ul><li>设置 Content-Type 和跨域访问响应头</li><li>遍历 EventMeshTCPServer 对象的 ClientSessionGroupMapping 和 Session 对象映射关系，将每个 Session 对象转换为 GetClientResponse 对象，并添加到列表 getClientResponseList 中。</li><li>对 getClientResponseList 按主机和端口进行排序。</li><li>将 getClientResponseList 转换为 JSON (result) 并发送响应。</li></ul><p><code>delete()</code> 方法：处理 DELETE 请求</p><ul><li>将请求体字符串 (request) 解析为 DeleteTCPClientRequest 对象。</li><li>从 deleteTCPClientRequest 中获取要删除的主机 (host) 和端口 (port)。</li><li>遍历 EventMeshTCPServer 对象的 ClientSessionGroupMapping 和 Session 对象映射关系，找到与要删除的主机和端口匹配的 Session 对象，并调用 EventMeshTcp2Client 的 serverGoodby2Client 方法，断开与该客户端的连接。</li><li>设置 Content-Type 和跨域访问响应头，然后发送 200 状态码的空响应。</li></ul><h4 id="client-http"><a href="#client-http" class="headerlink" title="/client/http"></a>/client/http</h4><p>位于 <code>HTTPClientHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/http</code>。</p><p>用于处理与 HTTP 客户端相关的管理操作，包括获取已连接的 HTTP 客户端列表和删除 HTTP 客户端。</p><p>与 <code>/client/tcp</code> 接口类似，不再赘述。</p><h4 id="client-grpc"><a href="#client-grpc" class="headerlink" title="/client/grpc"></a>/client/grpc</h4><p>位于 <code>GrpcClientHandler</code> 类，对应 eventmesh-dashboard 路径 <code>/grpc</code>。</p><p>用于处理与 gRPC 客户端相关的管理操作，包括获取已连接的 gRPC 客户端列表和删除 gRPC 客户端。</p><p>与 <code>/client/tcp</code> 接口类似，不再赘述。</p><h4 id="webhook-insertWebHookConfig"><a href="#webhook-insertWebHookConfig" class="headerlink" title="/webhook/insertWebHookConfig"></a>/webhook/insertWebHookConfig</h4><p>位于 <code>InsertWebHookConfigHandler</code> 类。以下接口均暂无 eventmesh-dashboard 的对应功能。</p><p>用于将 WebHook 配置插入到系统中。</p><p>在处理 HTTP 请求时，首先发送成功的响应头信息，然后解析请求体中的数据为 WebHookConfig 对象，接着调用 WebHookConfigOperation 对象的 insertWebHookConfig 方法将配置插入系统中，并将操作结果序列化为字符串并返回。</p><p><code>/webhook/updateWebHookConfig</code> 和 <code>/webhook/deleteWebHookConfig</code> 接口功能与此接口类似，分别对应了更新和删除操作，不再赘述。</p><h4 id="webhook-queryWebHookConfigById"><a href="#webhook-queryWebHookConfigById" class="headerlink" title="/webhook/queryWebHookConfigById"></a>/webhook/queryWebHookConfigById</h4><p>位于 <code>QueryWebHookConfigByIdHandler</code> 类。</p><p>用于根据 WebHook 配置 ID 查询相应的配置信息。</p><p>在处理 HTTP 请求时，首先发送成功的响应头信息，接着解析请求体中的数据为 WebHookConfig 对象，然后调用 WebHookConfigOperation 对象的 queryWebHookConfigById 方法查询配置信息，并将查询结果转换为 JSON 字符串写入输出流中返回给客户端。</p><p><code>/webhook/queryWebHookConfigByManufacturer</code> 接口与此接口类似，用于根据 WebHook 提供方（如 Github）分页查询相应的配置信息，不再赘述。</p><h4 id="eventMesh-recommend"><a href="#eventMesh-recommend" class="headerlink" title="/eventMesh/recommend"></a>/eventMesh/recommend</h4><p>位于 <code>QueryRecommendEventMeshHandler</code> 类。</p><p>用于查询推荐的 EventMesh 节点。根据传入的 group 和 purpose 参数，计算推荐的 EventMesh 节点，并返回结果。</p><p>在处理 HTTP 请求时，首先获取请求 URI 中的查询字符串，解析为键值对形式的 Map。然后检查注册中心是否启用，如果未启用则抛出异常。接着检查 group 和 purpose 参数是否为空，不为空就在计算推荐结果后，将结果序列化并写入输出流中返回。</p><h4 id="clientManage-redirectClientByIpPort"><a href="#clientManage-redirectClientByIpPort" class="headerlink" title="/clientManage/redirectClientByIpPort"></a>/clientManage/redirectClientByIpPort</h4><p>位于 <code>RedirectClientByIpPortHandler</code> 类。</p><p>用于根据传入的 IP 地址和端口将匹配的客户端重定向到目标的 EventMesh 节点。</p><p>在处理 HTTP 请求时，首先获取请求 URI 中的查询字符串，解析为键值对形式的 Map。然后检查参数的合法性，接着获取 EventMeshTCPServer 对象的 ClientSessionGroupMapping 对象和 Session 对象的映射关系。然后遍历 sessionMap 中的每个 Session 对象，如果 Session 对象的客户端的主机和端口与传入的 ip 和 port 匹配，就调用重定向方法，并将结果追加到重定向结果中。</p><p><code>/clientManage/redirectClientByPath</code> 和 <code>/clientManage/redirectClientBySubSystem</code> 接口功能与此接口类似，分别以路径和子系统进行匹配，不再赘述。</p><h4 id="clientManage-rejectClientByIpPort"><a href="#clientManage-rejectClientByIpPort" class="headerlink" title="/clientManage/rejectClientByIpPort"></a>/clientManage/rejectClientByIpPort</h4><p>位于 <code>RejectClientByIpPortHandler</code> 类。</p><p>用于根据传入的 IP 地址和端口将匹配的客户端连接拒绝。</p><p>在处理 HTTP 请求时，首先获取请求 URI 中的查询字符串，解析为键值对形式的 Map。然后检查参数的合法性，接着遍历映射关系，如果匹配就拒绝客户端连接，并将成功拒绝的远程地址加入成功远程地址列表。</p><p><code>/clientManage/rejectClientByPath</code> 和 <code>/clientManage/rejectClientBySubSystem</code> 接口功能与此接口类似，分别以路径和子系统进行匹配，不再赘述。</p><p><code>/clientManage/rejectAllClient</code> 接口则免去了匹配规则，直接拒绝所有客户端连接，同样不再赘述。</p><h4 id="clientManage-showClient"><a href="#clientManage-showClient" class="headerlink" title="/clientManage/showClient"></a>/clientManage/showClient</h4><p>用于查询所有客户端信息，并统计每个子系统中的客户端数量。</p><h4 id="clientManage-showListenClientByTopic"><a href="#clientManage-showListenClientByTopic" class="headerlink" title="/clientManage/showListenClientByTopic"></a>/clientManage/showListenClientByTopic</h4><p>位于 <code>ShowListenClientByTopicHandler</code> 类。</p><p>用于查询订阅特定主题的客户端信息。</p><p>对于每个客户端组，获取订阅了所查询的主题的会话集合 (listenSessionSet)，并将组名和每个会话的进程 ID、IP 地址、端口号、路径和版本作为结果返回。</p><p><code>/clientManage/showClientBySystem</code> 接口功能与此接口类似，以子系统进行匹配，不再赘述。</p><h3 id="方法分析"><a href="#方法分析" class="headerlink" title="方法分析"></a>方法分析</h3><p>此小节以 org.apache.eventmesh.admin.rocketmq 包中 <code>/topicManage</code> 接口的所有方法为例，进行关键代码逐行分析：</p><h4 id="AdminController"><a href="#AdminController" class="headerlink" title="AdminController"></a>AdminController</h4><ol><li><code>@Slf4j</code>: Lombok 注解，用于在编译时自动为类生成一个名为 <code>log</code> 的静态日志记录器。</li><li><code>public void run(HttpServer server) throws IOException</code>: 在该方法中，首先通过调用 <code>server.createContext(TOPIC_MANAGE_PATH, new TopicsHandler())</code> 创建上下文，并将其绑定到指定的路径 <code>TOPIC_MANAGE_PATH</code> 上。</li></ol><h4 id="TopicsHandler"><a href="#TopicsHandler" class="headerlink" title="TopicsHandler"></a>TopicsHandler</h4><h5 id="handle"><a href="#handle" class="headerlink" title="handle()"></a>handle()</h5><p>此方法用于处理 <code>HttpExchange</code> 对象。</p><ol><li>如果请求的 URL 匹配到 <code>TOPIC_MANAGE_PATH</code>，即满足 <code>RequestMapping.postMapping(TOPIC_MANAGE_PATH, httpExchange)</code> 条件，会调用 <code>createTopicHandler(httpExchange)</code> 方法来创建一个新的主题，并直接返回，不再执行后续的代码。</li><li>如果不匹配，就会首先获取 <code>httpExchange</code> 的输出流 <code>out</code>，用于向客户端发送响应数据。</li><li>然后调用 <code>httpExchange</code> 的 <code>sendResponseHeaders</code> 方法，将响应状态码设置为 500，表示服务器内部错误。</li><li>接着通过 <code>String.format</code> 方法构造一个错误提示信息。</li><li>使用日志记录器以 <code>error</code> 级别打印错误信息。</li><li>最后将错误提示信息转换为字节数组，通过输出流 <code>out</code> 将其发送给客户端。</li></ol><h5 id="createTopicHandler"><a href="#createTopicHandler" class="headerlink" title="createTopicHandler()"></a>createTopicHandler()</h5><p>此方法用于处理创建主题的请求。</p><ol><li>使用 <code>try-with-resources</code> 语句，创建一个输出流 <code>out</code>，并获取 <code>httpExchange</code> 对象的响应体输出流。</li><li>调用 <code>NetUtils.parsePostBody(httpExchange)</code> 方法解析 HTTP 请求体，并将结果存储在 <code>params</code> 变量中。</li><li>使用 <code>JsonUtils.parseObject(params, TopicCreateRequest.class)</code> 方法将 <code>params</code> 转换为 <code>TopicCreateRequest</code> 对象，随后获取要创建的主题 <code>topic</code>。</li><li>目前 <code>topicResponse</code> 对象的赋值为 <code>null</code>，因为这部分逻辑将来会实现一个新的 RocketMQ 服务来创建主题。</li><li>将 <code>topicResponse</code> 转换为 JSON 字符串，写入输出流 <code>out</code> 中，并将结果赋值给 <code>result</code>。</li></ol><h4 id="TopicCreateRequest"><a href="#TopicCreateRequest" class="headerlink" title="TopicCreateRequest"></a>TopicCreateRequest</h4><p>此类是用于表示创建主题的请求对象。</p><ol><li><code>@JsonInclude(JsonInclude.Include.NON_EMPTY)</code>: 此注解用于指定在将该对象转换为 JSON 格式时，忽略值为空的属性。</li><li><code>@JsonIgnoreProperties(ignoreUnknown = true)</code>: 此注解用于指定在将 JSON 转换为该对象时，忽略除 <code>topic</code> 以外的其他属性。</li><li><code>TopicCreateRequest(@JsonProperty("topic") String topic)</code>: 构造方法，通过 <code>@JsonProperty("topic")</code> 注解表示将传入的参数与 <code>topic</code> 属性进行绑定。</li></ol><h4 id="TopicResponse"><a href="#TopicResponse" class="headerlink" title="TopicResponse"></a>TopicResponse</h4><p>此类是用于表示主题响应的对象，提供了对主题名和创建时间的操作方法，并重写了 <code>toString</code> 方法。</p><ol><li><code>topic</code> 和 <code>createdTime</code> 分别用于存储主题名和创建时间。</li><li>构造函数使用 <code>@JsonCreator</code> 注解进行标记，意为该构造函数可以用于反序列化 JSON 对象并创建 <code>TopicResponse</code> 实例。</li><li>构造函数的参数使用 <code>@JsonProperty</code> 注解进行标记，指定了对应 JSON 属性的名称。</li></ol><h4 id="RequestMapping"><a href="#RequestMapping" class="headerlink" title="RequestMapping"></a>RequestMapping</h4><p>用于判断请求是否匹配。</p><ol><li><code>@UtilityClass</code>: Lombok 注解，用于生成一个带有私有构造方法和静态方法的工具类。</li><li><code>postMapping(String value, HttpExchange httpExchange)</code>: 用于处理 POST 请求，<code>value</code> 参数表示要匹配的 URL 路径，<code>HttpExchange</code> 参数表示 HTTP 请求的交换对象。返回一个布尔值，表示请求的 URL 路径是否与给定的 <code>value</code> 匹配。</li><li><code>getMapping()</code>,<code>putMapping()</code>,<code>deleteMapping()</code> 与 <code>postMapping()</code> 方法类似。</li><li><code>isUrlMatch(String value, HttpExchange httpExchange, String methodType)</code>: 用于判断请求的 URL 路径和方法类型是否与给定的 <code>value</code> 和 <code>methodType</code> 匹配。</li><li>如果类型匹配，就通过 <code>httpExchange.getRequestURI().getPath()</code> 获取请求的 URL 路径。</li><li>使用 <code>UrlMappingPattern</code> 对象 <code>matcher</code> 来进行请求路径与 <code>value</code> 的匹配。</li></ol><h4 id="UrlMappingPattern"><a href="#UrlMappingPattern" class="headerlink" title="UrlMappingPattern"></a>UrlMappingPattern</h4><p>用于处理 URL 匹配和提取路径参数值的辅助类。</p><ol><li><code>Map&lt;String, String&gt; extractPathParameterValues(String url)</code>: 用于从给定的 URL 中提取路径参数的值。它使用 <code>matcher.matches()</code> 方法来判断给定的 URL 是否与模式匹配。如果匹配成功，则调用 <code>extractParameters()</code> 方法提取路径参数的值并返回一个包含参数名和对应值的 <code>Map</code>。</li><li><code>Map&lt;String, String&gt; extractParameters(Matcher matcher)</code>: 用于从 <code>Matcher</code> 对象中提取路径参数的值。通过 <code>for</code> 循环遍历 <code>matcher</code> 第 <code>i+1</code> 个分组的值，即路径参数的值，随后将参数名和对应值存储在 <code>values</code> 中，最后返回包含路径参数和对应值的 <code>Map</code> 对象。</li></ol><h4 id="TopicResponseTest"><a href="#TopicResponseTest" class="headerlink" title="TopicResponseTest"></a>TopicResponseTest</h4><p>用于对 <code>TopicResponse</code> 类进行单元测试的测试类。</p><ol><li><code>@Test</code>: 用于标识测试方法的 JUnit 框架注解，JUnit 会自动识别并执行它们。</li><li><code>testTopicResponse()</code>: 用于测试 <code>TopicResponse</code> 类的构造函数和 getter 方法。首先创建一个 <code>TopicResponse</code> 对象，传入 <code>topic</code> 和 <code>createdTime</code> 参数。然后使用 <code>assertEquals()</code> 断言来验证对象的 <code>getTopic()</code> 和 <code>getCreatedTime()</code> 方法返回的值是否与输入的参数相等。</li><li><code>testTopicResponseSerialization()</code>: 用于测试 <code>TopicResponse</code> 类的序列化和反序列化。使用 <code>ObjectMapper</code> 将 <code>topicResponse</code> 对象序列化为 JSON 字符串，接着使用 <code>assertTrue()</code> 断言来验证 JSON 字符串中是否包含”topic” 和”created_time” 字段，然后使用 <code>ObjectMapper</code> 对象将 JSON 字符串反序列化为 <code>TopicResponse</code> 对象 <code>deserializedResponse</code>，最后使用 <code>assertEquals()</code> 断言来验证反序列化后的对象的 <code>getTopic()</code> 和 <code>getCreatedTime()</code> 方法返回的值是否与初始对象的值相等。</li></ol><h4 id="RequestMappingTest"><a href="#RequestMappingTest" class="headerlink" title="RequestMappingTest"></a>RequestMappingTest</h4><h5 id="testPostMapping"><a href="#testPostMapping" class="headerlink" title="testPostMapping()"></a>testPostMapping()</h5><ol><li><code>testPostMapping()</code>: 用于测试 <code>RequestMapping</code> 类的 <code>postMapping</code> 方法。在该方法中，首先创建了一个 <code>HttpExchange</code> 对象的 mock 实例，用来模拟 HTTP 请求和响应的交互，然后使用 <code>when()</code> 方法和 <code>thenReturn()</code> 方法设置模拟对象的行为。</li><li><code>testGetMapping()</code>,<code>testPutMapping()</code>,<code>testDeleteMapping()</code> 与 <code>testPostMapping()</code> 方法类似。</li></ol><h2 id="实施方案"><a href="#实施方案" class="headerlink" title="实施方案"></a>实施方案</h2><h3 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h3><h4 id="范围"><a href="#范围" class="headerlink" title="范围"></a>范围</h4><p>对于课题预期任务而言，需要编写注释的范围在于 org.apache.eventmesh.admin.rocketmq 和 org.apache.eventmesh.runtime.admin 这两个包，对象为所有的方法。</p><h4 id="格式"><a href="#格式" class="headerlink" title="格式"></a>格式</h4><p>对于任何一个项目而言，尤其是开源项目，在撰写 JavaDoc 注释时，都需要注意以下方面，以确保注释全面且易于理解：</p><ol><li>摘要（Summary）：提供一个简洁但清晰的摘要，概括该方法或接口的主要功能和作用。</li><li>参数（Parameters）：列出方法或接口接受的所有参数，并为每个参数提供描述。包括参数的名称、类型、是否可为空以及对参数的期望值或用法的说明。</li><li>返回值（Return Value）：描述方法或接口的返回值。指明返回值的类型、可能的返回结果、异常情况或特殊条件等。</li><li>抛出（Throws）：列出方法或接口可能会抛出的异常，并提供每个异常的类型、触发条件和处理建议。</li><li>示例（Examples）：提供一个或多个示例，展示如何使用该方法或接口。可以包括参数设置、方法调用和预期结果的演示。</li><li>注意事项（Notes）：说明任何与方法或接口相关的重要注意事项或限制。</li><li>作者（Author）：标明编写该方法或接口的作者。</li><li>参考（See Also）：指向与该方法或接口相关的其他文档、资源或类。</li><li>版本（Version）：指明该方法或接口首次出现的版本号，并注明修改历史和版本更新。</li><li>修饰符（Modifiers）：指明方法或接口的访问修饰符（例如 public、private、protected）和其他修饰符（例如 static、final）。</li><li>参数范围（Parameter Ranges）：为每个参数提供有效范围或允许的取值范围。</li><li>线程安全性（Thread Safety）：指明该方法或接口的线程安全性信息。例如是否可以在多线程环境中安全地调用。</li><li>依赖关系（Dependencies）：列出方法或接口依赖的其他类、接口或资源。</li></ol><p>具体来说，Apache EventMesh 的项目仓库中，在 style/checkStyle.xml 路径提供了代码样式检查文件，需要在 IntelliJ IDEA 中安装 CheckStyle-IDEA 插件配合使用，通过以下方式导入检查样式文件：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Editor -&gt; Code Style -&gt; Java -&gt; Scheme -&gt; Import Scheme -&gt; CheckStyle Configuration</span><br></pre></td></tr></tbody></table></figure><p>然后使用<code>./gradlew check</code> 来检查代码风格。</p><p>在这个代码样式文件中，规定了 Apache EventMesh 项目所偏好的 JavaDoc 注释风格，需要：</p><ol><li><p>对齐形参说明</p></li><li><p>对齐抛出异常说明</p></li><li><p>在描述后空行</p></li><li><p>保留无效标签</p></li><li><p>保留空 @param 标签</p></li><li><p>保留空 @return 标签</p></li><li><p>保留空 @throws 标签</p></li><li><p>在右页边距处换行</p></li><li><p>启用前导星号</p></li><li><p>用 @throws 而不是 @exception</p></li><li><p>在空行中生成 <code>&lt;p&gt;</code></p></li><li><p>保留空行</p></li></ol><p>不需要：</p><ol><li>在形参描述后空行</li><li>在 return 后空行</li><li>一行注释不分行</li><li>保留换行</li><li>在新行描述形参</li><li>缩进连续线</li></ol><p>示例如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> sample;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Sample</span> {</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 这是一个方法的描述，如果其长度长到超出右边界，</span></span><br><span class="line"><span class="comment">     * &lt;p&gt;</span></span><br><span class="line"><span class="comment">     * 就需要另起一行，在新的段落继续描述。</span></span><br><span class="line"><span class="comment">     * &lt;p/&gt;</span></span><br><span class="line"><span class="comment">     * 可以手动换行。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> i                  简短命名的参数描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> longParameterName  长命名的参数描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> missingDescription 缺少描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 返回描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> XXXException 异常描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> YException   异常描述</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> ZException</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@invalidTag</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod</span><span class="params">(<span class="type">int</span> i,</span></span><br><span class="line"><span class="params">                                        <span class="type">int</span> longParameterName,</span></span><br><span class="line"><span class="params">                                        <span class="type">int</span> missingDescription)</span></span><br><span class="line">            <span class="keyword">throws</span> XXXException, YException, ZException;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 单行注释</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod2</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 简单方法描述</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">abstract</span> String <span class="title function_">sampleMethod3</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h4 id="提交形式"><a href="#提交形式" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个功能所包含的文件与类为单位，在 apache/eventmesh 仓库新建一个 Issue，声明正在为哪个模块的哪个功能撰写注释，然后向 Pil0tXia/eventmesh 仓库的 pil0txia_doc_{ISSUE ID} 分支提交 Git Commit。当一个主要功能的全部接口和方法的 JavaDoc 注释均已撰写完成时，从该分支向 apache/eventmesh 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 apache/eventmesh 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成一个 Contributor 与 Reviewer 异步的贡献形式。</p><h3 id="文档"><a href="#文档" class="headerlink" title="文档"></a>文档</h3><h4 id="范围-1"><a href="#范围-1" class="headerlink" title="范围"></a>范围</h4><p>对于课题预期任务而言，需要编写文档的范围在于 org.apache.eventmesh.admin.rocketmq 和 org.apache.eventmesh.runtime.admin 这两个包，对象为所有的接口。</p><h4 id="格式-1"><a href="#格式-1" class="headerlink" title="格式"></a>格式</h4><p>Apache EventMesh 项目使用 Docusaurus 架构作为其官网文档的 Web 前端框架。此框架使用 Markdown 语言作为文档的撰写与解析语言。</p><p>在正式创建 md 文件之前，需要先思考接口文档在官网文档的侧边栏目录中的位置和组织形式，并且在 eventmesh-site/i18n/zh/docusaurus-plugin-content-docs/current.json 文件中添加属于接口文档的栏目。</p><p>eventmesh.apache.org 支持英文与中文两种语言，这两种语言的 Markdown 文件是分开存放的。在 eventmesh-site 仓库中：</p><ul><li><code>docs/design-document</code> 目录：该目录是英文版的文档目录，用于存放英文版的设计文档和其他相关文档。</li><li><code>i18n/zh/docusaurus-plugin-content-docs/current/design-document</code> 目录：该目录是中文版的文档目录，用于存放中文版的设计文档和其他相关文档。<code>i18n</code> 表示国际化，<code>zh</code> 表示中文，<code>docusaurus-plugin-content-docs</code> 是 Docusaurus 生成的目录结构，<code>current</code> 表示当前版本的文档。</li></ul><p>这两个目录中的层级是一样的，是为了支持多语言的文档展示。</p><p>在编写文档时，需要注意以下方面，以确保 Markdown 语法可以被正确地解析，并支持多种 Markdown 渲染器的排版：</p><ol><li>目录结构：根据接口的层级结构或逻辑关系，创建一个清晰的目录结构。使用标题和子标题来组织接口文档，且标题层级不超过四级。</li><li>接口概述：对每个接口提供一个简要概述，描述其用途、输入和输出等关键信息。指明接口的名称、路径和 HTTP 方法。</li><li>参数说明：列出每个接口所需的参数，并提供参数的名称、类型、是否必需、取值范围以及示例值等信息。对于复杂的参数结构，可以使用表格或嵌套列表来清晰展示参数的层级关系和说明。</li><li>响应示例：提供一个或多个示例，展示接口的调用和返回结果。示例可以包括请求和响应的数据结构、状态码和消息等信息。对于可选的响应字段，也可以提供示例值。</li><li>异常处理：描述可能的错误情况和异常，以及相应的错误码和错误消息。提供每个异常的名称、描述和处理建议。</li><li>接口详情：为每个接口提供更详细的说明，包括接口的功能、用法、限制、注意事项和最佳实践等。可以使用段落、列表和代码块来组织和展示信息。</li><li>参考资料：提供与该模块或接口相关的其他文档、资源或链接。</li><li>更新记录：在文档中提供更新记录和重要变更，指明版本号、修改内容和日期。</li><li>示例代码：为关键接口或复杂场景提供示例代码。</li><li>格式和排版：使用代码块和强调样式等来保持一致的格式和排版。</li><li>图表和图像：可以使用图表、图像或流程图等可视化工具来说明接口的工作流程或数据流动。</li><li>文档导航：在 Docusaurus 官网上发布时，需要在整个网站上提供简单且直观的目录导航，使访问者能够轻松找到和浏览 admin 模块的接口文档。</li></ol><p>在编写 Markdown 文档之前，我应该已经在接口的代码中撰写了注释，以便从代码中对照文档。</p><h4 id="提交形式-1"><a href="#提交形式-1" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个接口为单位，在 apache/eventmesh-site 仓库新建一个 Issue，声明正在为哪个接口撰写注释，然后向 Pil0tXia/eventmesh-site 仓库的 pil0txia_docs_{ISSUE ID} 分支提交 Git Commit。此处的分支名称与撰写注释任务的分支名称并不相同，发布于 Web 网站上的使用文档的英文通常使用 docs 表示，与承载撰写注释任务的 doc 分支作区分。</p><p>当一个主要功能的全部接口的 Markdown 文档均已撰写完成时，从该分支向 apache/eventmesh-site 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 apache/eventmesh-site 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成一个 Contributor 与 Reviewer 异步的贡献形式。</p><h4 id="示例：获取系统配置信息"><a href="#示例：获取系统配置信息" class="headerlink" title="示例：获取系统配置信息"></a>示例：获取系统配置信息</h4><h5 id="接口说明"><a href="#接口说明" class="headerlink" title="接口说明"></a>接口说明</h5><ul><li>本接口用于获取当前 EventMesh 节点的基本配置信息，包括服务名称、服务环境和各协议监听端口地址等。</li></ul><h5 id="请求地址"><a href="#请求地址" class="headerlink" title="请求地址"></a>请求地址</h5><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET http://localhost:10106/configuration</span><br></pre></td></tr></tbody></table></figure><h5 id="请求参数说明"><a href="#请求参数说明" class="headerlink" title="请求参数说明"></a>请求参数说明</h5><table><thead><tr><th align="left">参数</th><th align="left">类型</th><th align="left">必填</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">无</td><td align="left"></td><td align="left"></td><td align="left"></td></tr></tbody></table><p><strong>请求示例:</strong></p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">无</span><br></pre></td></tr></tbody></table></figure><h5 id="返回参数说明"><a href="#返回参数说明" class="headerlink" title="返回参数说明"></a>返回参数说明</h5><table><thead><tr><th align="left">参数</th><th align="left">类型</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left"> sysID</td><td align="left">String</td><td align="left"> 系统 ID，用于标识系统的唯一标识符。</td></tr><tr><td align="left">namesrvAddr</td><td align="left">String</td><td align="left">Apache RocketMQ 的 NameServer 的 IP 地址和端口号。</td></tr><tr><td align="left">eventMeshEnv</td><td align="left">String</td><td align="left">EventMesh 运行的环境，如生产环境或开发环境。</td></tr><tr><td align="left">eventMeshIDC</td><td align="left">String</td><td align="left">EventMesh 所在的数据中心标识</td></tr><tr><td align="left"> eventMeshCluster</td><td align="left">String</td><td align="left">EventMesh 所属的集群名称</td></tr><tr><td align="left"> eventMeshServerIp</td><td align="left">String</td><td align="left">EventMesh 服务所在服务器的 IP 地址</td></tr><tr><td align="left"> eventMeshName</td><td align="left">String</td><td align="left">EventMesh 的名称</td></tr><tr><td align="left"> eventMeshWebhookOrigin</td><td align="left">String</td><td align="left">EventMesh Webhook 的默认来源</td></tr><tr><td align="left"> eventMeshServerSecurityEnable</td><td align="left">boolean</td><td align="left"> 是否启用安全功能</td></tr><tr><td align="left"> eventMeshServerRegistryEnable</td><td align="left">boolean</td><td align="left"> 是否启用注册功能</td></tr><tr><td align="left"> eventMeshTcpServerPort</td><td align="left">int32</td><td align="left"> 监听 TCP 连接的端口号</td></tr><tr><td align="left"> eventMeshHttpServerPort</td><td align="left">int32</td><td align="left"> 监听 HTTP 请求的端口号</td></tr><tr><td align="left"> eventMeshHttpServerUseTls</td><td align="left">boolean</td><td align="left">HTTP 请求是否启用 TLS 加密协议</td></tr><tr><td align="left"> eventMeshGrpcServerPort</td><td align="left">int32</td><td align="left"> 监听 gRPC 请求的端口号</td></tr><tr><td align="left"> eventMeshGrpcServerUseTls</td><td align="left">boolean</td><td align="left">gRPC 请求是否启用 TLS 加密协议</td></tr></tbody></table><p><strong>返回结果示例：</strong></p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"sysID"</span><span class="punctuation">:</span> <span class="string">"0000"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"namesrvAddr"</span><span class="punctuation">:</span> <span class="string">"127.0.0.1:8848"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshEnv"</span><span class="punctuation">:</span> <span class="string">"PRD"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshIDC"</span><span class="punctuation">:</span> <span class="string">"DEFAULT"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshCluster"</span><span class="punctuation">:</span> <span class="string">"COMMON"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshServerIp"</span><span class="punctuation">:</span> <span class="string">"10.0.34.190"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshName"</span><span class="punctuation">:</span> <span class="string">"EVENTMESH-runtime"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshWebhookOrigin"</span><span class="punctuation">:</span> <span class="string">"eventmesh.DEFAULT"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshServerSecurityEnable"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshServerRegistryEnable"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshTcpServerPort"</span><span class="punctuation">:</span> <span class="number">10000</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshHttpServerPort"</span><span class="punctuation">:</span> <span class="number">10105</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshHttpServerUseTls"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshGrpcServerPort"</span><span class="punctuation">:</span> <span class="number">10205</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"eventMeshGrpcServerUseTls"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><h5 id="返回码说明"><a href="#返回码说明" class="headerlink" title="返回码说明"></a>返回码说明</h5><table><thead><tr><th align="left">返回码</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left"> 200</td><td align="left"> 查询成功</td></tr><tr><td align="left"> 500</td><td align="left"> 服务端错误</td></tr></tbody></table><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><h4 id="范围-2"><a href="#范围-2" class="headerlink" title="范围"></a>范围</h4><p>org.apache.eventmesh.admin.rocketmq 和 org.apache.eventmesh.runtime.admin 两个包，对象为所有的方法。</p><h4 id="要求"><a href="#要求" class="headerlink" title="要求"></a>要求</h4><p>测试代码的编写可以参考现有的测试文件。在单元测试时，需要注意以下方面：</p><ol><li>输入验证：确保测试覆盖了各种可能的输入情况，包括边界值、无效值、空值和有效值。</li><li>接口状态：测试应该涵盖接口的各种状态和路径。例如，在测试 <code>testTopicCreateRequestSetName</code> 时，应该包括设置名称为 <code>null</code> 的情况以验证接口的响应。</li><li>异常情况：测试接口在异常情况下的行为。例如接口是否正确地处理了错误的输入或不正确的参数。</li><li>序列化和反序列化：对于涉及对象的序列化和反序列化的接口，应该编写测试来验证对象的正确序列化和反序列化。确保序列化后的数据包含所需的字段，并且在反序列化后可以正确还原为对象。</li><li>副作用和一致性：如果接口执行了一些副作用或对系统状态进行了更改，测试应该验证这些副作用是否按预期发生，并确保接口的行为一致。</li><li>测试覆盖率：尽量覆盖接口的各个路径和代码分支，以确保测试足够全面。使用代码覆盖工具（如 JaCoCo 和 Codecov 等）来评估测试的覆盖率，并尽量达到较高的覆盖率。</li><li>并发和性能：如果接口设计要求支持高并发或高性能，需要验证接口在高并发和高负载情况下的表现和性能。</li><li>引入依赖：在测试中正确模拟或提供依赖项。</li><li>可读性和可维护性：编写清晰、简洁、可读性强的测试代码，使用有意义的命名和注释，以便他人能够理解和维护测试。</li><li>持续集成和自动化：将测试集成到持续集成（CI）流程中，在每次代码提交时自动运行测试。</li></ol><h4 id="提交形式-2"><a href="#提交形式-2" class="headerlink" title="提交形式"></a>提交形式</h4><p>以一个功能所包含的文件与类为单位，在 apache/eventmesh 仓库新建一个 Issue，声明正在为哪个模块的哪个功能编写测试代码，然后向 Pil0tXia/eventmesh 仓库的 pil0txia_test_{ISSUE ID} 分支提交 Git Commit。当一个主要功能的全部接口和方法的 JavaDoc 注释均已撰写完成时，从该分支向 apache/eventmesh 仓库发起 Pull Request，并请求 Commiters 和 Maintainers 进行 Code Review，进行代码合并。</p><p>当拉取合并请求处于 Review 阶段时，我将从 apache/eventmesh 仓库 master 分支最新的提交拉取一个新的分支，并继续按照上述工作流新建 Issue、撰写注释，形成 Contributor 与 Reviewer 异步的贡献形式。</p><h3 id="增强"><a href="#增强" class="headerlink" title="增强"></a>增强</h3><h4 id="整合-admin-模块"><a href="#整合-admin-模块" class="headerlink" title="整合 admin 模块"></a>整合 admin 模块</h4><p>虽然现有的 admin 模块对事件存储的管理接口耦合在 runtime 运行时中，但是 eventmesh-admin 模块中的接口与 org.apache.eventmesh.runtime.admin 包中的接口都遵循 controller 路由、handler 实现、request 实体、response 实体和 utils 工具类的基本布局，因此在技术层面上的整合更多的需要考虑与主线开发者的冲突问题和接口可用性问题。</p><h5 id="与主线开发者的冲突问题"><a href="#与主线开发者的冲突问题" class="headerlink" title="与主线开发者的冲突问题"></a>与主线开发者的冲突问题</h5><p>将 org.apache.eventmesh.runtime.admin 包中的接口移走必然会导致与其它开发者在此包中的修改产生 Git 冲突。</p><p>为了避免冲突数量过多、过于复杂、难以解决，我将在开始此任务前使用 <code>git rebase</code> 同步主线进度，并尽快完成所有迁移整合。</p><p>在开发阶段性完成时，我将再次使用变基合并。相比于全部整合完成后再使用 <code>merge</code> 合并，这种方式的好处在于单次合并冲突数量少、分支提交记录线性排列较为清晰、联系另一位开发者解决冲突的缓冲时间长、不容易影响工作进度。</p><h5 id="接口可用性问题"><a href="#接口可用性问题" class="headerlink" title="接口可用性问题"></a>接口可用性问题</h5><p>接口在整合后可能会产生隐性的问题，尤其是与作用域相关的调用问题。为此，我将利用 IntelliJ IDEA 的 <code>yFiles</code> 图表功能，保留接口整合前各类注解、导入、抽象类和依赖包的引用关系截图，与整合后的引用关系相比较，确保作用域一致。</p><p>对于接口本身功能是否正常的自测，将使用 apache/eventmesh-dashboard 前端管理中心、Postman 和测试单元配合完成。</p><p>对于 eventmesh-dashboard 前端上具有的接口，将在获取数据格式后，使用 Postman 多次验证不同的传入值，并确保单元测试通过。</p><p>对于 eventmesh-dashboard 前端上不具有的接口，我将在上面方法的基础上，仔细分析代码，使用 Postman 得出正确的数据格式。</p><h4 id="远程启停集群节点"><a href="#远程启停集群节点" class="headerlink" title="远程启停集群节点"></a>远程启停集群节点</h4><h5 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h5><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.api.model.Pod;</span><br><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.api.model.PodList;</span><br><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.client.DefaultKubernetesClient;</span><br><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.client.KubernetesClient;</span><br><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.client.KubernetesClientException;</span><br><span class="line"><span class="keyword">import</span> io.fabric8.kubernetes.client.dsl.PodResource;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EventMeshAdmin</span> {</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">EVENTMESH_NAMESPACE</span> <span class="operator">=</span> <span class="string">"k8s-namespace"</span>; <span class="comment">// Kubernetes命名空间</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">EVENTMESH_LABEL</span> <span class="operator">=</span> <span class="string">"app=eventmesh"</span>; <span class="comment">// EventMesh标签</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line">        <span class="type">EventMeshAdmin</span> <span class="variable">admin</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">EventMeshAdmin</span>();</span><br><span class="line">        admin.startEventMeshNode(<span class="string">"eventmesh-1"</span>); <span class="comment">// 启动指定的EventMesh节点</span></span><br><span class="line">        admin.stopEventMeshNode(<span class="string">"eventmesh-2"</span>); <span class="comment">// 停止指定的EventMesh节点</span></span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">startEventMeshNode</span><span class="params">(String nodeName)</span> {</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">KubernetesClient</span> <span class="variable">client</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultKubernetesClient</span>()) {</span><br><span class="line">            <span class="comment">// 获取符合标签选择器的Pod列表</span></span><br><span class="line">            <span class="type">PodList</span> <span class="variable">podList</span> <span class="operator">=</span> client.pods().inNamespace(EVENTMESH_NAMESPACE).withLabel(EVENTMESH_LABEL).list();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 查找指定节点的Pod并启动</span></span><br><span class="line">            <span class="keyword">for</span> (Pod pod : podList.getItems()) {</span><br><span class="line">                <span class="keyword">if</span> (pod.getMetadata().getName().equals(nodeName)) {</span><br><span class="line">                    PodResource&lt;Pod&gt; podResource = client.pods()</span><br><span class="line">                            .inNamespace(EVENTMESH_NAMESPACE)</span><br><span class="line">                            .withName(pod.getMetadata().getName());</span><br><span class="line"></span><br><span class="line">                    <span class="comment">// 更新Pod的副本数量为1以启动</span></span><br><span class="line">                    podResource.scale(<span class="number">1</span>, <span class="literal">true</span>);</span><br><span class="line">                    System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' started."</span>);</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                }</span><br><span class="line">            }</span><br><span class="line"></span><br><span class="line">            System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' not found."</span>);</span><br><span class="line">        } <span class="keyword">catch</span> (KubernetesClientException e) {</span><br><span class="line">            System.err.println(<span class="string">"Error occurred while starting EventMesh node: "</span> + e.getMessage());</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">stopEventMeshNode</span><span class="params">(String nodeName)</span> {</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">KubernetesClient</span> <span class="variable">client</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultKubernetesClient</span>()) {</span><br><span class="line">            <span class="comment">// 获取符合标签选择器的Pod列表</span></span><br><span class="line">            <span class="type">PodList</span> <span class="variable">podList</span> <span class="operator">=</span> client.pods().inNamespace(EVENTMESH_NAMESPACE).withLabel(EVENTMESH_LABEL).list();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 查找指定节点的Pod并停止它</span></span><br><span class="line">            <span class="keyword">for</span> (Pod pod : podList.getItems()) {</span><br><span class="line">                <span class="keyword">if</span> (pod.getMetadata().getName().equals(nodeName)) {</span><br><span class="line">                    PodResource&lt;Pod&gt; podResource = client.pods()</span><br><span class="line">                            .inNamespace(EVENTMESH_NAMESPACE)</span><br><span class="line">                            .withName(pod.getMetadata().getName());</span><br><span class="line"></span><br><span class="line">                    <span class="comment">// 更新Pod的副本数量为0以停止</span></span><br><span class="line">                    podResource.scale(<span class="number">0</span>, <span class="literal">true</span>);</span><br><span class="line">                    System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' stopped."</span>);</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                }</span><br><span class="line">            }</span><br><span class="line"></span><br><span class="line">            System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' not found."</span>);</span><br><span class="line">        } <span class="keyword">catch</span> (KubernetesClientException e) {</span><br><span class="line">            System.err.println(<span class="string">"Error occurred while stopping EventMesh node: "</span> + e.getMessage());</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h5 id="方案介绍"><a href="#方案介绍" class="headerlink" title="方案介绍"></a>方案介绍</h5><p>当 EventMesh 节点以 docker 方式部署在 Kubernetes 中时，可以使用以下思路远程启停集群节点：</p><ol><li>客户端库：使用 Java Kubernetes 客户端库 fabric8io/kubernetes-client 与 Kubernetes 集群进行交互，管理 Pod、服务和副本集。</li><li>标签选择器：使用 Kubernetes 的标签选择器来选择具有特定标签的 Pod，即可对指定的 EventMesh 节点执行操作。</li><li>节点启停：使用 Pod 的副本数量来控制节点的启停，当 Pod 的副本数量为 1 或更多时就会启动节点，当副本数量为 0 时就会停止节点，有效利用了 Kubernetes 的横向扩展和自动伸缩特性。</li></ol><p>在以上示例代码中，使用了 <code>DefaultKubernetesClient</code> 创建一个与 Kubernetes 集群的连接，并通过标签选择器获取与 <code>app=eventmesh</code> 匹配的 Pod 列表。然后遍历 Pod 列表，找到与指定节点名称相匹配的 Pod，并使用 <code>PodResource</code> 对象对该 Pod 进行启动或停止操作。</p><p>异常处理方面，使用了 try-with-resources 语句来自动关闭 KubernetesClient 连接，并在 catch 块中捕获 KubernetesClientException。</p><h4 id="远程启停单机节点"><a href="#远程启停单机节点" class="headerlink" title="远程启停单机节点"></a>远程启停单机节点</h4><h5 id="示例代码-1"><a href="#示例代码-1" class="headerlink" title="示例代码"></a>示例代码</h5><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.io.BufferedReader;</span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.io.InputStreamReader;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EventMeshAdmin</span> {</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">REMOTE_SERVER_IP</span> <span class="operator">=</span> <span class="string">"eventmesh-server-ip"</span>; <span class="comment">// EventMesh服务器的IP地址</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">REMOTE_SERVER_USERNAME</span> <span class="operator">=</span> <span class="string">"eventmesh-server-username"</span>; <span class="comment">// EventMesh服务器的用户名</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line">        <span class="type">EventMeshAdmin</span> <span class="variable">admin</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">EventMeshAdmin</span>();</span><br><span class="line">        admin.startEventMeshNode(<span class="string">"eventmesh-1"</span>);</span><br><span class="line">        admin.stopEventMeshNode(<span class="string">"eventmesh-2"</span>);</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">startEventMeshNode</span><span class="params">(String nodeName)</span> {</span><br><span class="line">        <span class="keyword">try</span> {</span><br><span class="line">            <span class="type">String</span> <span class="variable">command</span> <span class="operator">=</span> <span class="string">"ssh "</span> + REMOTE_SERVER_USERNAME + <span class="string">"@"</span> + REMOTE_SERVER_IP + <span class="string">" sh /path/to/eventmesh-runtime/bin/start.sh "</span> + nodeName;</span><br><span class="line">            executeCommand(command);</span><br><span class="line">            System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' started."</span>);</span><br><span class="line">        } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line">            System.err.println(<span class="string">"Error occurred while starting EventMesh node: "</span> + e.getMessage());</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">stopEventMeshNode</span><span class="params">(String nodeName)</span> {</span><br><span class="line">        <span class="keyword">try</span> {</span><br><span class="line">            <span class="type">String</span> <span class="variable">command</span> <span class="operator">=</span> <span class="string">"ssh "</span> + REMOTE_SERVER_USERNAME + <span class="string">"@"</span> + REMOTE_SERVER_IP + <span class="string">" sh /path/to/eventmesh-runtime/bin/stop.sh "</span> + nodeName;</span><br><span class="line">            executeCommand(command);</span><br><span class="line">            System.out.println(<span class="string">"EventMesh node '"</span> + nodeName + <span class="string">"' stopped."</span>);</span><br><span class="line">        } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line">            System.err.println(<span class="string">"Error occurred while stopping EventMesh node: "</span> + e.getMessage());</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">executeCommand</span><span class="params">(String command)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line">        <span class="type">ProcessBuilder</span> <span class="variable">processBuilder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ProcessBuilder</span>(command.split(<span class="string">" "</span>));</span><br><span class="line">        <span class="type">Process</span> <span class="variable">process</span> <span class="operator">=</span> processBuilder.start();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 读取命令执行结果</span></span><br><span class="line">        <span class="type">BufferedReader</span> <span class="variable">reader</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">InputStreamReader</span>(process.getInputStream()));</span><br><span class="line">        String line;</span><br><span class="line">        <span class="keyword">while</span> ((line = reader.readLine()) != <span class="literal">null</span>) {</span><br><span class="line">            System.out.println(line);</span><br><span class="line">        }</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 检查命令执行是否成功</span></span><br><span class="line">        <span class="keyword">try</span> {</span><br><span class="line">            <span class="type">int</span> <span class="variable">exitCode</span> <span class="operator">=</span> process.waitFor();</span><br><span class="line">            <span class="keyword">if</span> (exitCode != <span class="number">0</span>) {</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IOException</span>(<span class="string">"Command execution failed with exit code: "</span> + exitCode);</span><br><span class="line">            }</span><br><span class="line">        } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IOException</span>(<span class="string">"Command execution interrupted: "</span> + e.getMessage());</span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h5 id="方案介绍-1"><a href="#方案介绍-1" class="headerlink" title="方案介绍"></a>方案介绍</h5><p>在项目代码库中，有 start.sh 和 stop.sh 两个用于单机部署的 Shell 脚本，可以在本地环境启停 EventMesh 服务。相对于前面的 Kubernetes 客户端库方案，此方案适用于单机部署的场景。</p><p>以上示例代码主要通过远程执行脚本的方式实现启动和停止操作，远程服务器需要支持 SSH 服务，并具有脚本文件的可执行权限。其优点在于执行结果的输出将保持原样返回给调用方，可以得到更具体的报错信息，缺点在于无法灵活地运用中间件在集群部署中的优势，也无法利用 Kubernetes 客户端库实现节点的扩容和缩容、监控节点状态，获取节点日志等功能。</p><h4 id="更新管理脚本"><a href="#更新管理脚本" class="headerlink" title="更新管理脚本"></a>更新管理脚本</h4><h5 id="修改建议"><a href="#修改建议" class="headerlink" title="修改建议"></a>修改建议</h5><ol><li><p>提取通用的环境变量和 JVM 配置，放在脚本的顶部，便于统一管理和修改。比如 <code>export LANG=en_US.UTF-8</code>、<code>export LC_CTYPE=en_US.UTF-8</code>、<code>export LC_ALL=en_US.UTF-8</code> 这些语句，和 <code>JAVA_OPT</code> 中的一些通用配置项，可以放在脚本的开头。</p></li><li><p>在脚本中加入一些错误处理机制，比如在 <code>JAVA</code> 路径未找到时输出错误信息并退出。</p></li><li><p>使用 <code>pgrep</code> 命令优化获取进程 PID 的复杂度。</p></li><li><p>可以使用更简洁的方式创建日志文件夹。例如可以使用 <code>mkdir -p "${EVENTMESH_LOG_HOME}"</code> 一行代码替换当前的路径。</p></li><li><p>可以在启动命令前后添加一些额外的操作。例如，可以在启动命令执行之前打印一些额外的信息，或在启动命令执行之后将进程 ID 保存到文件中。</p></li></ol><h5 id="示例函数优化"><a href="#示例函数优化" class="headerlink" title="示例函数优化"></a>示例函数优化</h5><figure class="highlight shell"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">function get_pid {</span><br><span class="line">    local ppid=""</span><br><span class="line">    if [ -f "${EVENTMESH_HOME}/bin/pid.file" ]; then</span><br><span class="line">        ppid=$(cat "${EVENTMESH_HOME}/bin/pid.file")</span><br><span class="line">    else</span><br><span class="line">        if [[ $OS =~ (Msys|Darwin) ]]; then</span><br><span class="line">            ppid=$(jps -v | awk -v pattern="org.apache.eventmesh.runtime.boot.EventMeshStartup" '$0 ~ pattern &amp;&amp; $0 !~ /grep/ {print $1; exit}')</span><br><span class="line">        else</span><br><span class="line">            ppid=$(ps -C java -o user,pid,command --cols 99999 --no-header | awk -v home="$EVENTMESH_HOME" -v pattern="org.apache.eventmesh.runtime.boot.EventMeshStartup" '$0 ~ pattern &amp;&amp; $0 !~ /grep/ &amp;&amp; $0 ~ home {print $2; exit}')</span><br><span class="line">        fi</span><br><span class="line">    fi</span><br><span class="line">    echo "$ppid"</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><strong>变更说明：</strong></p><ol><li>在文件路径中添加双引号，避免可能的空格或特殊字符导致的问题。</li><li>使用更简洁的语法将 <code>$OS</code> 的值匹配到 <code>Msys</code> 或 <code>Darwin</code>。</li><li>优化 <code>jps</code> 命令的使用，使用 <code>awk</code> 进行过滤和提取，避免多次使用 <code>grep</code> 命令。</li><li>在 <code>ps</code> 命令中添加了 <code>--no-header</code> 去除标题行选项。</li><li>使用 <code>awk</code> 提取 PID 时，添加 <code>$0 !~ /grep/</code> 条件来排除 <code>grep</code> 进程。</li><li>对于 <code>ps</code> 命令，添加 <code>$0 ~ home</code> 条件来确保进程命令行中包含 <code>$EVENTMESH_HOME</code> 路径。</li></ol><h4 id="开发-TBD-和-TODO"><a href="#开发-TBD-和-TODO" class="headerlink" title="开发 TBD 和 TODO"></a>开发 TBD 和 TODO</h4><p>对于新增的功能，我将在熟悉需求后，自行建立业务场景，针对场景中的细节开发每一项对应功能，并编写单元测试，确保接口功能正常、可靠。</p><p>例如，在 org.apache.eventmesh.admin.rocketmq.util.UrlMappingPatternTest 中，有一个要求编写测试方法的 TODO：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">//TODO : Fix me to test the method compile(). It is better using Mockito not PowerMockito.</span><br></pre></td></tr></tbody></table></figure><p>使用 Mockito，并通过 Java 反射获取私有字段，撰写正则表达式，示例代码如下：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCompile</span><span class="params">()</span> <span class="keyword">throws</span> NoSuchFieldException, IllegalAccessException {</span><br><span class="line">    <span class="comment">// Mock the compiledUrlMappingPattern field with reflection</span></span><br><span class="line">    <span class="type">Pattern</span> <span class="variable">mockedPattern</span> <span class="operator">=</span> mock(Pattern.class);</span><br><span class="line">    <span class="type">Field</span> <span class="variable">compiledUrlMappingPatternField</span> <span class="operator">=</span> urlMappingPattern.getClass().getDeclaredField(<span class="string">"compiledUrlMappingPattern"</span>);</span><br><span class="line">    compiledUrlMappingPatternField.setAccessible(<span class="literal">true</span>);</span><br><span class="line">    compiledUrlMappingPatternField.set(urlMappingPattern, mockedPattern);</span><br><span class="line"></span><br><span class="line">    urlMappingPattern.compile();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Verify that the compiledUrlMappingPattern field is updated</span></span><br><span class="line">    assertEquals(mockedPattern, compiledUrlMappingPatternField.get(urlMappingPattern));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Verify that the mocked pattern is compiled with the expected regex</span></span><br><span class="line">    Mockito.verify(mockedPattern).compile(<span class="string">"/test/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)/path/([%\\w-.\\~!$&amp;'\\(\\)\\*\\+,;=:\\[\\]@]+?)(?:\\?.*?)?$"</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>调试后，预期与实际相符，测试用例通过。</p><h2 id="时间规划"><a href="#时间规划" class="headerlink" title="时间规划"></a>时间规划</h2><h3 id="每周时间安排"><a href="#每周时间安排" class="headerlink" title="每周时间安排"></a>每周时间安排</h3><p>每周约 32 小时：</p><ul><li>周一至周五，每日 3 小时</li><li>周末，每日 8 小时</li><li>向导师汇报开发进度与安排，1 小时</li></ul><h3 id="项目进度"><a href="#项目进度" class="headerlink" title="项目进度"></a>项目进度</h3><table><thead><tr><th align="center">任务</th><th align="center">时间</th></tr></thead><tbody><tr><td align="center">熟悉项目</td><td align="center"> 7.1 - 7.7</td></tr><tr><td align="center"> 编写 JavaDoc 注释</td><td align="center"> 7.8 - 7.14</td></tr><tr><td align="center"> 编写接口文档</td><td align="center"> 7.15 - 7.21</td></tr><tr><td align="center"> 测试现有接口功能</td><td align="center"> 7.21 - 8.4</td></tr><tr><td align="center"> 撰写中期报告</td><td align="center"> 8.5 - 8.11</td></tr><tr><td align="center"> 整合 admin 模块</td><td align="center"> 8.12 - 8.25</td></tr><tr><td align="center"> 增加管理命令</td><td align="center"> 8.25 - 9.7</td></tr><tr><td align="center"> 更新管理脚本</td><td align="center"> 9.8 - 9.14</td></tr><tr><td align="center"> 开发 TBD 和 TODO</td><td align="center">9.15 - 9.21</td></tr><tr><td align="center"> 撰写结题报告</td><td align="center"> 9.22 - 10.5</td></tr><tr><td align="center"> 弹性时间安排</td><td align="center"> 10.5 - 10.11</td></tr></tbody></table><h2 id="个人简介"><a href="#个人简介" class="headerlink" title="个人简介"></a>个人简介</h2><p>我是来自南京信息工程大学的夏天，大三，目前正在联想实习，承担 Spring Cloud + Kafka + Eureka 方面的后端开发工作。这是我的<a href="https://www.pil0txia.com/">博客</a>、<a href="https://docs.pil0txia.com/">文档</a>和 <a href="https://github.com/Pil0tXia">Github</a>，日均 PV 400 左右，有些文章的谷歌 / 必应排名也比较高。</p><p>每每使用开源工具和框架，都很感谢开发者的付出。在我注册 Github 账号的第五年，我意识到自己应该真正地去研究透彻一个框架、参与一个社区、进行贡献，我也非常希望自己能在时间还算充裕的学生时代，多尝试一些新技术，抓住这个契机。</p><p>这么几天的探索下来，兴趣越发高涨，衷心希望能参加薛炜明导师您指导的 GLCC 课题。</p><p>联系方式：<a href="mailto:admin@pil0txia.com">admin@pil0txia.com</a></p><h2 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h2><p>在后续的社区贡献中，我会深入理解产品定位，设想产品场景，主动发现增长点与增强点，持之以恒地作出贡献。</p><p>既然目前 EventMesh 已经支持了比较多的事件存储了，那么不同的事件存储之间的数据也可以支持互相传输，这样使用 EventMesh SDK 即可打通各事件存储。</p><p>不过，虽然 EventMesh 可以支持多种事件存储，但每一种事件存储的支持都需要使用连接器插件与事件存储耦合，且不同的事件存储需要使用不同的连接器。为了减少连接器插件的使用方法更新时业务侧的代码变动，可以开发一个支持统一各事件存储的通用连接器。形象化来说，其工作原理类似于 “插座”。业务侧只需要在各个事件存储的 Client SDK 中添加通用的连接器即可，而无需关注各事件存储或连接器插件在更新后的代码改动。EventMesh 方面需要适配不同的插件。也就是说，业务侧只需要对这个通用连接器负责，而原有的连接器插件只需要对 “插座” 负责，插座只需要对所有的事件存储负责，大幅降低了业务侧的代码改动频率。</p><h1 id="中期考核答辩"><a href="#中期考核答辩" class="headerlink" title="中期考核答辩"></a>中期考核答辩</h1><div style="position: relative; width: 100%; height: 0; padding-bottom: 75%;"><iframe src="//player.bilibili.com/player.html?aid=829738300&amp;bvid=BV19u4y1X7eR&amp;cid=1232076610&amp;page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"> </iframe></div><blockquote><p>8-15 更新：🆒</p><p><img src="https://static.pil0txia.com/picgo/image-20230815163241345.webp" alt="image-20230815163241345"></p></blockquote><h1 id="结项考核答辩"><a href="#结项考核答辩" class="headerlink" title="结项考核答辩"></a>结项考核答辩</h1><div style="position: relative; width: 100%; height: 0; padding-bottom: 75%;"><iframe src="//player.bilibili.com/player.html?aid=234196701&amp;bvid=BV168411C7VZ&amp;cid=1287867365&amp;p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"> </iframe></div><blockquote><p>10-08 更新：💯</p><p><img src="https://static.pil0txia.com/picgo/image-20231008181351805.webp" alt="image-20231008181351805"></p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;6-27 更新：已中选，能从海内外名校候选者中脱颖而出是一件很荣幸的事情，感谢导师的信任，Apache 邮箱我来了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://static.pil0txia.com/picgo/image-20230627</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="GLCC" scheme="https://www.pil0txia.com/tags/GLCC/"/>
    
    <category term="Proposal" scheme="https://www.pil0txia.com/tags/Proposal/"/>
    
  </entry>
  
  <entry>
    <title>【开源贡献笔记】确保线程在阻塞状态被中断的 InterruptedException 得到处理</title>
    <link href="https://www.pil0txia.com/post/2023-06-18_handle-interrupted-exception/"/>
    <id>https://www.pil0txia.com/post/2023-06-18_handle-interrupted-exception/</id>
    <published>2023-06-17T16:02:28.000Z</published>
    <updated>2023-06-17T16:24:28.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h2><p>issue 来自 Apache EventMesh，Github 链接：<a href="https://github.com/apache/eventmesh/issues/4110">[Enhancement] InterruptedExceptions should never be ignored in the code.[HttpRetryer]</a></p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">dispatcher = <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; {</span><br><span class="line">            <span class="keyword">try</span> {</span><br><span class="line">                DelayRetryable retryObj;</span><br><span class="line">                <span class="keyword">while</span> (!Thread.currentThread().isInterrupted() &amp;&amp; (retryObj = failed.take()) != <span class="literal">null</span>) {</span><br><span class="line">                    <span class="keyword">final</span> <span class="type">DelayRetryable</span> <span class="variable">delayRetryable</span> <span class="operator">=</span> retryObj;</span><br><span class="line">                    pool.execute(() -&gt; {</span><br><span class="line">                        <span class="keyword">try</span> {</span><br><span class="line">                            delayRetryable.retry();</span><br><span class="line">                            <span class="keyword">if</span> (retryLogger.isDebugEnabled()) {</span><br><span class="line">                                retryLogger.debug(<span class="string">"retryObj : {}"</span>, delayRetryable);</span><br><span class="line">                            }</span><br><span class="line">                        } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">                            retryLogger.error(<span class="string">"http-retry-dispatcher error!"</span>, e);</span><br><span class="line">                        }</span><br><span class="line">                    });</span><br><span class="line">                }</span><br><span class="line">            } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">                retryLogger.error(<span class="string">"http-retry-dispatcher error!"</span>, e);</span><br><span class="line">            }</span><br><span class="line">        }, <span class="string">"http-retry-dispatcher"</span>);</span><br><span class="line">        dispatcher.setDaemon(<span class="literal">true</span>);</span><br><span class="line">        log.info(<span class="string">"HttpRetryer inited......"</span>);</span><br></pre></td></tr></tbody></table></figure><blockquote><p>InterruptedExceptions 在代码中不应该被忽略，在这种情况下，简单地记录异常也算作 “忽略”。抛出 InterruptedException 会清除线程的中断状态，所以如果异常处理不当，线程被中断的信息就会丢失。相反，InterruptedExceptions 应该被重新抛出–立即或在清理方法的状态之后–或者通过调用 Thread.interrupt () 来重新中断线程，即使这应该是一个单线程的应用程序。任何其他行为都有可能延迟线程关闭，并丢失线程被中断的信息–可能没有完成其任务。</p></blockquote><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><p>在 #4110 这个 case 中，我以前只在多线程应用中显式处理过 InterruptedExceptions。如果线程在阻塞状态被中断，为了在处理中断后不让后续代码产生错误判断，所以抛出 InterruptedException 的同时会调用 Thread.interrupted () 方法来清除线程的中断状态。</p><p>不过 issue 所提到的 dispatcher 是一个单线程应用，如果它在执行 take () 方法时被中断，就会捕获 InterruptedException 异常，然后继续执行异常处理块，此时应该不会丢失线程被中断的信息。但是因为 InterruptedException 是一个 checked exception，如果不对其进行处理，它就会被传播到方法的调用者，有可能会在 EventMeshHTTPServer.java 抛出，从而导致线程的延迟关闭。加之此时也没有及时退出或恢复中断状态，线程可能会继续执行 retry () 方法，进而丢失线程被中断的信息。</p><p>但是 issue 里说 InterruptedExceptions 应该被重新抛出，我觉得不合适。这样处理没有重新设置线程的中断状态，也没有向其他开发者传达线程被中断的意图和语义。重新设置中断状态会更好。</p><p>当然，想必开发者也清楚这一点～</p><h2 id="修改方案"><a href="#修改方案" class="headerlink" title="修改方案"></a>修改方案</h2><p>Merged 已合并：<a href="https://github.com/apache/eventmesh/pull/4113">[ISSUE #4110] Enhance thread handling of InterruptedException by Pil0tXia · Pull Request #4113 · apache/eventmesh</a></p><p><img src="https://static.pil0txia.com/picgo/image-20230618012847499.webp" alt="image-20230618012847499"></p><blockquote><p>If a thread is interrupted while in a blocked state, in order to prevent subsequent code from making erroneous judgments after handling the interruption, the <code>InterruptedException</code> is thrown and at the same time, the <code>Thread.interrupted()</code> method is called to clear the thread’s interrupt status.</p><p>If the dispatcher is interrupted while executing the <code>take()</code> method, it will catch the <code>InterruptedException</code> exception and continue executing the exception handling block. At this point, the information about the thread being interrupted should not be lost. However, because <code>InterruptedException</code> is a <code>checked exception</code>, if it is not handled, it will be propagated to the method caller and may be thrown in <code>EventMeshHTTPServer.java</code>, resulting in a delayed closure of the thread. Moreover, there is no timely exit or restoration of the interrupt status at this point, so the thread may continue executing the <code>retry()</code> method, thereby losing the information about the thread being interrupted.</p><p>However, the issue states that <code>InterruptedExceptions</code> should be re-thrown, but I think this handling is inappropriate. This approach does not reset the thread’s interrupt status nor does it communicate the intention and semantics of the thread being interrupted to other developers. It would be better to reset the interrupt status.</p></blockquote><p>捕获 InterruptedException 异常并重新中断线程：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">    <span class="keyword">if</span> (e <span class="keyword">instanceof</span> InterruptedException) {</span><br><span class="line">        Thread.currentThread().interrupt();</span><br><span class="line">    }</span><br><span class="line">    retryLogger.error(<span class="string">"http-retry-dispatcher error!"</span>, e);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;问题背景&quot;&gt;&lt;a href=&quot;#问题背景&quot; class=&quot;headerlink&quot; title=&quot;问题背景&quot;&gt;&lt;/a&gt;问题背景&lt;/h2&gt;&lt;p&gt;issue 来自 Apache EventMesh，Github 链接：&lt;a href=&quot;https://github.co</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="开源" scheme="https://www.pil0txia.com/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Apache" scheme="https://www.pil0txia.com/tags/Apache/"/>
    
    <category term="EventMesh" scheme="https://www.pil0txia.com/tags/EventMesh/"/>
    
    <category term="多线程" scheme="https://www.pil0txia.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    
    <category term="阻塞" scheme="https://www.pil0txia.com/tags/%E9%98%BB%E5%A1%9E/"/>
    
    <category term="中断" scheme="https://www.pil0txia.com/tags/%E4%B8%AD%E6%96%AD/"/>
    
    <category term="异常" scheme="https://www.pil0txia.com/tags/%E5%BC%82%E5%B8%B8/"/>
    
  </entry>
  
  <entry>
    <title>【联想实习笔记】如何优雅地处理数据表中一对多的重复记录</title>
    <link href="https://www.pil0txia.com/post/2023-06-09_handle-database-one-to-many-records/"/>
    <id>https://www.pil0txia.com/post/2023-06-09_handle-database-one-to-many-records/</id>
    <published>2023-06-09T05:57:59.000Z</published>
    <updated>2023-06-09T06:34:50.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>语句已做脱敏处理。方言基于 SAP HANA。</p></blockquote><p>如果数据库表中存在一对多的记录，这段 SQL 会报错 <code>SAP DBTech JDBC: [305]: single-row query returns more than one row</code>。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">(</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_MENU smps</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line"><span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line">) <span class="keyword">AS</span> MENU_ID,</span><br><span class="line"><span class="number">3</span> <span class="keyword">AS</span> "TYPE",</span><br><span class="line">(</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_USER sus</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">USER_NAME <span class="operator">=</span> <span class="string">'myUsername'</span></span><br><span class="line">) <span class="keyword">AS</span> OWNER</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">DUMMY;</span><br></pre></td></tr></tbody></table></figure><h2 id="用LIMIT提取第一行"><a href="#用LIMIT提取第一行" class="headerlink" title="用LIMIT提取第一行"></a>用 LIMIT 提取第一行</h2><p>如果我们只需要第一条记录，一对多的其它数据都无所谓，只需要在子查询中加上 <code>LIMIT 1</code> 即可。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">(</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_MENU smps</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line"><span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line">    LIMIT <span class="number">1</span>) <span class="keyword">AS</span> MENU_ID,</span><br><span class="line"><span class="number">3</span> <span class="keyword">AS</span> "TYPE",</span><br><span class="line">(</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_USER sus</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">USER_NAME <span class="operator">=</span> <span class="string">'myUsername'</span></span><br><span class="line">LIMIT <span class="number">1</span>) <span class="keyword">AS</span> OWNER</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">DUMMY;</span><br></pre></td></tr></tbody></table></figure><h2 id="用ROW-NUMBER-提取任意行"><a href="#用ROW-NUMBER-提取任意行" class="headerlink" title="用ROW_NUMBER()提取任意行"></a>用 ROW_NUMBER () 提取任意行</h2><p>如果是我们需要对多行结果集中的每一条单条记录分别进行处理的场景，可以使用 <code>ROW_NUMBER()</code> 和 <code>OVER()</code> 窗口函数分步处理。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span></span><br><span class="line"><span class="keyword">FROM</span> (</span><br><span class="line">  <span class="keyword">SELECT</span> <span class="operator">*</span>,</span><br><span class="line">         <span class="built_in">ROW_NUMBER</span>() <span class="keyword">OVER</span> (<span class="keyword">PARTITION</span> <span class="keyword">BY</span> column1, column2 <span class="keyword">ORDER</span> <span class="keyword">BY</span> id) <span class="keyword">AS</span> row_num</span><br><span class="line">  <span class="keyword">FROM</span> your_table</span><br><span class="line">) <span class="keyword">AS</span> subquery</span><br><span class="line"><span class="keyword">WHERE</span> row_num <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></tbody></table></figure><p>但这往往没有 Java 或 Mybatis 中的 foreach 更方便。</p><h2 id="用LEFT-JOIN替代子查询"><a href="#用LEFT-JOIN替代子查询" class="headerlink" title="用LEFT JOIN替代子查询"></a>用 LEFT JOIN 替代子查询</h2><p>对于数据库表中有一对多的记录，而我们需要一次性对所有数据进行处理的场景，要想办法避免无法接受多行结果集的函数或用法。</p><p>修改前，子查询是在一个列的上下文中执行的，并且被用作列表达式的一部分。在这种情况下，每个子查询必须返回单个值，因为它们被设计为用于标量子查询（scalar subquery）。如果子查询返回多个行，那么在这个位置上就无法解析这个子查询，因为每个行都需要一个值来填充 <code>MENU_ID</code> 这一列。换句话说，子查询应该返回一个标量值（单个值），而不是一个结果集（多个行）。</p><p>修改后，即使表中有一对多的记录，也可以将所有记录统一返回，而不是报错。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">smps.ID <span class="keyword">AS</span> MENU_ID,</span><br><span class="line"><span class="number">3</span> <span class="keyword">AS</span> "TYPE",</span><br><span class="line">sus.ID <span class="keyword">AS</span> OWNER</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_MENU smps</span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span> SYS_USER sus <span class="keyword">ON</span></span><br><span class="line">sus.USER_NAME <span class="operator">=</span> <span class="string">'myUsername'</span></span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">smps.NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line"><span class="keyword">AND</span> smps.HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span>;</span><br></pre></td></tr></tbody></table></figure><p>然而，我需要进一步的为 <code>smps.ID</code> 和 <code>sus.ID</code> 添加 <code>COALESCE</code> 函数，当 <code>smps.ID</code> 或 <code>sus.ID</code> 为 null 时，可以使用 <code>USER_NAME</code> 或 <code>HREF</code> 作为替代值。使用 LEFT JOIN 后，当右侧条件 <code>WHERE LOWER(USER_NAME) = LOWER('myUsername')</code> 匹配不到结果时，<code>sus_ID</code> 可以显示替代值，当左侧条件 <code>WHERE NAME = 'My Report Name'</code> 在表中没有任何匹配结果时，整个查询只会返回空结果集，就无法用 <code>COALESCE</code> 函数对 <code>MENU_ID</code> 插入替代值了。即使使用 FULL JOIN 或笛卡尔积 CROSS JOIN 也无法解决任何一边的结果集为空的情况。</p><h2 id="用UNION-ALL确保不返回空"><a href="#用UNION-ALL确保不返回空" class="headerlink" title="用UNION ALL确保不返回空"></a>用 UNION ALL 确保不返回空</h2><p>为了解决这个问题，可以用 LEFT JOIN 将两个子查询连接在一起，并且 ON 条件始终为 true。然后在每个子查询的末尾添加一个 UNION ALL 子句，如果没有检查到匹配结果，就会通过 <code>DUMMY</code> 表添加一行带有 NULL 值的记录。这样即可确保即使 URL 没有匹配结果，仍然会返回带有替代值的结果。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">    <span class="built_in">COALESCE</span>(TO_CHAR(smps.ID), <span class="string">'/path/to/my/report'</span>) <span class="keyword">AS</span> MENU_ID,</span><br><span class="line">    <span class="built_in">COALESCE</span>(TO_CHAR(sus.ID), <span class="string">'myUsername'</span>) <span class="keyword">AS</span> OWNER</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">    (<span class="keyword">SELECT</span> ID</span><br><span class="line">    <span class="keyword">FROM</span> SYS_MENU</span><br><span class="line">    <span class="keyword">WHERE</span> NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line">        <span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line">    <span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line">    <span class="keyword">SELECT</span> <span class="keyword">NULL</span> <span class="keyword">AS</span> ID</span><br><span class="line">    <span class="keyword">FROM</span> DUMMY</span><br><span class="line">    <span class="keyword">WHERE</span> <span class="keyword">NOT</span> <span class="keyword">EXISTS</span> (</span><br><span class="line">        <span class="keyword">SELECT</span> ID</span><br><span class="line">        <span class="keyword">FROM</span> SYS_MENU</span><br><span class="line">        <span class="keyword">WHERE</span> NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line">            <span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line">    )) <span class="keyword">AS</span> smps</span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span></span><br><span class="line">    (<span class="keyword">SELECT</span> ID</span><br><span class="line">    <span class="keyword">FROM</span> SYS_USER</span><br><span class="line">    <span class="keyword">WHERE</span> <span class="built_in">LOWER</span>(USER_NAME) <span class="operator">=</span> <span class="built_in">LOWER</span>(<span class="string">'myUsername'</span>)</span><br><span class="line">    <span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line">    <span class="keyword">SELECT</span> <span class="keyword">NULL</span> <span class="keyword">AS</span> ID</span><br><span class="line">    <span class="keyword">FROM</span> DUMMY</span><br><span class="line">    <span class="keyword">WHERE</span> <span class="keyword">NOT</span> <span class="keyword">EXISTS</span> (</span><br><span class="line">        <span class="keyword">SELECT</span> ID</span><br><span class="line">        <span class="keyword">FROM</span> SYS_USER</span><br><span class="line">        <span class="keyword">WHERE</span> <span class="built_in">LOWER</span>(USER_NAME) <span class="operator">=</span> <span class="built_in">LOWER</span>(<span class="string">'myUsername'</span>)</span><br><span class="line">    )) <span class="keyword">AS</span> sus</span><br><span class="line"><span class="keyword">ON</span> <span class="number">1</span> <span class="operator">=</span> <span class="number">1</span>;</span><br></pre></td></tr></tbody></table></figure><p>最终我使用的 SQL 语句如下，再结合 Excel 公式和 SQL 转义，它完美地帮我将一份<code>报表名称与URL重复/不对应、用户名不准确/大小写混淆、存在隐性的/无法匹配的Unicode字符</code>的 xlsx 表格维护的 MENU 与 USER 的关系导入到了数据库中：上万数据一遍跑通，新数据更新旧数据，哪里匹配不上就留下问题定位信息，一对多记录也一并插入，还可以重复导入，最后手工纠正一下替代值，甚是畅快。</p><figure class="highlight sql"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">MERGE</span></span><br><span class="line"><span class="keyword">INTO</span></span><br><span class="line">SYS_MENU_USER <span class="keyword">AS</span> target</span><br><span class="line"><span class="keyword">USING</span> (</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line"><span class="built_in">COALESCE</span>(TO_CHAR(smps.ID),</span><br><span class="line"><span class="string">'/path/to/my/report'</span>) <span class="keyword">AS</span> MENU_ID,</span><br><span class="line"><span class="number">3</span> <span class="keyword">AS</span> "TYPE",</span><br><span class="line"><span class="built_in">COALESCE</span>(TO_CHAR(sus.ID),</span><br><span class="line"><span class="string">'myUsername'</span>) <span class="keyword">AS</span> OWNER</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">(</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_MENU</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line"><span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line"><span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line"><span class="keyword">NULL</span> <span class="keyword">AS</span> ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">DUMMY</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line"><span class="keyword">NOT</span> <span class="keyword">EXISTS</span> (</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_MENU</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">NAME <span class="operator">=</span> <span class="string">'My Report Name'</span></span><br><span class="line"><span class="keyword">AND</span> HREF <span class="operator">=</span> <span class="string">'/path/to/my/report'</span></span><br><span class="line">    )) <span class="keyword">AS</span> smps</span><br><span class="line"><span class="keyword">LEFT</span> <span class="keyword">JOIN</span></span><br><span class="line">    (</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_USER</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line"><span class="built_in">LOWER</span>(USER_NAME) <span class="operator">=</span> <span class="built_in">LOWER</span>(<span class="string">'myUsername'</span>)</span><br><span class="line"><span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line"><span class="keyword">NULL</span> <span class="keyword">AS</span> ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">DUMMY</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line"><span class="keyword">NOT</span> <span class="keyword">EXISTS</span> (</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">ID</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">SYS_USER</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line"><span class="built_in">LOWER</span>(USER_NAME) <span class="operator">=</span> <span class="built_in">LOWER</span>(<span class="string">'myUsername'</span>)</span><br><span class="line">    )) <span class="keyword">AS</span> sus</span><br><span class="line"><span class="keyword">ON</span></span><br><span class="line"><span class="number">1</span> <span class="operator">=</span> <span class="number">1</span>) <span class="keyword">AS</span> SOURCE</span><br><span class="line"><span class="keyword">ON</span></span><br><span class="line">target.MENU_ID <span class="operator">=</span> source.MENU_ID</span><br><span class="line"><span class="keyword">AND</span> target."TYPE" <span class="operator">=</span> source."TYPE"</span><br><span class="line"><span class="keyword">WHEN</span> MATCHED <span class="keyword">THEN</span></span><br><span class="line">    <span class="keyword">UPDATE</span></span><br><span class="line"><span class="keyword">SET</span></span><br><span class="line">OWNER <span class="operator">=</span> source.OWNER</span><br><span class="line"><span class="keyword">WHEN</span> <span class="keyword">NOT</span> MATCHED <span class="keyword">THEN</span></span><br><span class="line"><span class="keyword">INSERT</span></span><br><span class="line">(MENU_ID,</span><br><span class="line">"TYPE",</span><br><span class="line">OWNER)</span><br><span class="line"><span class="keyword">VALUES</span> (source.MENU_ID,</span><br><span class="line">source."TYPE",</span><br><span class="line">source.OWNER);</span><br></pre></td></tr></tbody></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;语句已做脱敏处理。方言基于 SAP HANA。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果数据库表中存在一对多的记录，这段 SQL 会报错 &lt;code&gt;SAP DBTech JDBC: [305]: single-row query retur</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.pil0txia.com/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="实习" scheme="https://www.pil0txia.com/tags/%E5%AE%9E%E4%B9%A0/"/>
    
    <category term="HANA" scheme="https://www.pil0txia.com/tags/HANA/"/>
    
    <category term="SQL优化" scheme="https://www.pil0txia.com/tags/SQL%E4%BC%98%E5%8C%96/"/>
    
    <category term="一对多" scheme="https://www.pil0txia.com/tags/%E4%B8%80%E5%AF%B9%E5%A4%9A/"/>
    
  </entry>
  
  <entry>
    <title>【联想实习笔记】分步查询真的一定比联表查询更好吗？</title>
    <link href="https://www.pil0txia.com/post/2023-06-02_join-table-query-or-step-by-step-query/"/>
    <id>https://www.pil0txia.com/post/2023-06-02_join-table-query-or-step-by-step-query/</id>
    <published>2023-06-02T09:47:15.000Z</published>
    <updated>2023-06-09T05:47:15.000Z</updated>
    
    <content type="html"><![CDATA[<p>虽然分步查询<strong>代码可复用性高、更利于后续的维护、可以更好的利用缓存</strong>，但是阿里规范禁止三张表 JOIN 的最迫不得已的原因为：</p><ul><li>去 IOE 行动后，MySQL 优化器和执行器很弱，多表连接性能不如 Oracle</li><li> 单表在 MySQL 中数据量太大，由于它的索引结构设计没有针对大表，所以查询性能会断崖式下滑</li><li>不得不考虑分库分表 + 中间件的模型，但跨库 JOIN 的性能受网络 IO 瓶颈，除非业务能够很好的根据 sharding key 明确要 Join 的两个表在同一个物理库中</li><li>如果将 Join 放到中间件去实现，由于中间件拿到数据 sharding 信息更难，成本肯定更大</li><li>中间件一般对跨库 join 都支持不好。在分库分表中，要同步更新两个表，这两个表位于不同的物理库中，为了保证数据一致性，一种做法是通过分布式事务中间件将两个更新操作放到一个事务中，但这样的操作一般要加全局锁，性能很捉急，而有些业务能够容忍短暂的数据不一致，怎么做？让它们分别更新呗，但是会存在数据写失败的问题，那就起个定时任务，扫描下 A 表有没有失败的行，然后看看 B 表是不是也没写成功，然后对这两条关联记录做订正，这个时候同样没法用 join 去实现，只能将数据拉到 service 层应用自己来合并了</li></ul><p>这些规则都是互联网开发团队总结出来的，适用于高并发、轻写重读、分布式、业务逻辑简单的情况，甚至对数据的一致性要求都不高，允许脏读（毕竟 web 是个非实时、无状态的东西）。</p><p>而对于很多低并发、<strong>频繁复杂数据写入、CPU 密集而非 IO 密集、主要业务逻辑通过数据库处理、甚至包含大量存储过程、对一致性与完整性要求很高</strong>的系统。比如金融、财务、企业应用之类，复杂 Join 也是不可避免的，<strong>不仅要写，还要写好</strong>，才能发挥数据库最大的功用。</p><p>在我的 Team 中，有很多 DB 同事，为 Java 开发写好了 SAP HANA 数据库的存储过程，进行了大量数据清洗，理应利用高效的内存数据库优势。此外，由于 SCI 系统的内存资源比较紧张，一个微服务在容器云上目前最多只能分配 10G，同时业务表数据量比较巨大，一个 version 几千万的数据是很正常的，如果分步查询，每个步骤的获取的结果都需要拿到 JVM 内存中与下一步的结果去做处理，会比较占用内存。因此很多报表的处理思路都是能在 SQL 中解决的就在 SQL 中解决，Java 代码中不做复杂的逻辑。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;虽然分步查询&lt;strong&gt;代码可复用性高、更利于后续的维护、可以更好的利用缓存&lt;/strong&gt;，但是阿里规范禁止三张表 JOIN 的最迫不得已的原因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去 IOE 行动后，MySQL 优化器和执行器很弱，多表连接性能不如 Oracle&lt;/li</summary>
      
    
    
    
    <category term="数据库" scheme="https://www.pil0txia.com/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="实习" scheme="https://www.pil0txia.com/tags/%E5%AE%9E%E4%B9%A0/"/>
    
    <category term="MySQL" scheme="https://www.pil0txia.com/tags/MySQL/"/>
    
    <category term="Oracle" scheme="https://www.pil0txia.com/tags/Oracle/"/>
    
    <category term="HANA" scheme="https://www.pil0txia.com/tags/HANA/"/>
    
    <category term="SQL优化" scheme="https://www.pil0txia.com/tags/SQL%E4%BC%98%E5%8C%96/"/>
    
    <category term="联表查询" scheme="https://www.pil0txia.com/tags/%E8%81%94%E8%A1%A8%E6%9F%A5%E8%AF%A2/"/>
    
    <category term="分步查询" scheme="https://www.pil0txia.com/tags/%E5%88%86%E6%AD%A5%E6%9F%A5%E8%AF%A2/"/>
    
  </entry>
  
  <entry>
    <title>编译原理学习笔记</title>
    <link href="https://www.pil0txia.com/post/2023-05-08_compilation-principle-notes/"/>
    <id>https://www.pil0txia.com/post/2023-05-08_compilation-principle-notes/</id>
    <published>2023-05-08T09:04:17.000Z</published>
    <updated>2023-05-08T09:34:17.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="不讲不考察的内容"><a href="#不讲不考察的内容" class="headerlink" title="不讲不考察的内容"></a>不讲不考察的内容</h2><blockquote><p>参考：<a href="https://zhangt.top/CS/Compilation-Principles-Study-Notes/">编译原理 - 学习笔记 - Zhang T’s Blog</a></p></blockquote><ul><li><p>1.5 编译程序的生成</p></li><li><p>第七章 作用域</p></li><li><p>第八章</p></li></ul><h2 id="日常课堂笔记"><a href="#日常课堂笔记" class="headerlink" title="日常课堂笔记"></a>日常课堂笔记</h2><h3 id="非确定集与确定集的分辨"><a href="#非确定集与确定集的分辨" class="headerlink" title="非确定集与确定集的分辨"></a>非确定集与确定集的分辨</h3><p>一个状态（字符集合）在接收到同样的字符串时，会转变到不同的状态（集合），导致了它的非确定性。</p><h3 id="NFA转换为DFA"><a href="#NFA转换为DFA" class="headerlink" title="NFA转换为DFA"></a>NFA 转换为 DFA</h3><p>所谓的状态闭包是指，从当前节点，经过 ε 符（无条件转移）能到达的所有节点的集合，包括当前节点自己。</p><h4 id="分解"><a href="#分解" class="headerlink" title="分解"></a>分解</h4><p>A -&gt; 1 -&gt; B 指 B 从 A 接收 1</p><p>self 0 指该节点有一个指向自己的、以接收到的字符为 0 为条件的无限循环</p><p>1/0 指该节点有两条指向下一节点的、分别以 0 和 1 为条件的状态转移</p><p>ε 不代表什么都接收，而是无条件转移的意思</p><ol><li><p>A -&gt; 1(0|1)*(1|0) -&gt; B</p><p>A -&gt; 1 -&gt; C -&gt; E(self 0/1) -&gt; D -&gt; 1/0 -&gt; B</p></li><li><p>A -&gt; 1*(0|1)0* -&gt; B</p><p>A -&gt; C(self 1) -&gt; C -&gt; 0/1 -&gt; D -&gt; E(self 0)  -&gt; B</p></li></ol><h4 id="状态转移矩阵"><a href="#状态转移矩阵" class="headerlink" title="状态转移矩阵"></a>状态转移矩阵</h4><p>第一行 I 是起始点的状态闭包，在求第一行 I<del>a</del> 的时候，除了要列出可以从 I 中某个元素接收 a 的节点，还要在全部列出后补充 I<del>a</del> 这些节点的状态闭包，即可以从 I<del>a</del> 某个节点通过 ε 前往的节点。</p><p>然后，新的集合添加到下一行，进行重复的步骤。</p><p>最后，为每一行从头到尾添加节点编号，进行化简。</p><h4 id="DFA化简"><a href="#DFA化简" class="headerlink" title="DFA化简"></a>DFA 化简</h4><p>将终止态和非终止态分开</p><p>用非终止态开始一个一个字符开始测试</p><p>测试完得到一些划分</p><p>两个字符到同一个状态集合就算等价，不一定只能是到一个状态</p><p>例如，{3,4,5,6} 中的 3,4,5 接收 a 不到达同一个状态，那就也不是同一个功能，需要分开。</p><h3 id="LR-0"><a href="#LR-0" class="headerlink" title="LR(0)"></a>LR(0)</h3><p><a href="https://blog.csdn.net/weixin_44225182/article/details/105554383">编译原理学习笔记（七）~LR (0) 分析_海轰 Pro 的博客 - CSDN 博客</a></p><h3 id="LR-1"><a href="#LR-1" class="headerlink" title="LR(1)"></a>LR(1)</h3><p>在 LR (0) 的基础后面加 #,{c,d} 等</p><h3 id="LL-1"><a href="#LL-1" class="headerlink" title="LL(1)"></a>LL(1)</h3><h2 id="期中考试复习课"><a href="#期中考试复习课" class="headerlink" title="期中考试复习课"></a>期中考试复习课</h2><h3 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h3><p>词法分析包括：标识符、常数、字符串、关键字、界符</p><p>中间代码、优化器 会出简答题</p><p>出错处理：词法分析中关键词与标识符冲突、界符写错（没有大括号），打印 / 显示出来</p><p>表格管理：不断地在每个步骤生成结果，保存到表格中</p><p>一个典型的编译器通常由以下几个模块组成，每个模块都有不同的功能和任务：</p><ol><li>词法分析器：也称为扫描器（Scanner），它是编译器的第一个模块，负责将源代码中的字符序列转换为有意义的单词（Token）序列，并为每个 Token 分配一个 Token 种类（Token Type）。</li><li>语法分析器：也称为解析器（Parser），它是编译器的第二个模块，负责将 Token 序列转换为抽象语法树（AST）或语法分析树，以及对语法错误进行检测和报告。</li><li>语义分析器：它是编译器的第三个模块，负责对 AST 进行类型检查、语义检查、符号表管理等操作，以及生成中间代码或目标代码。</li><li>中间代码生成器：它是编译器的第四个模块，负责将 AST 或符号表转换为中间表示（IR），并进行优化和优化。</li><li>目标代码生成器：它是编译器的第五个模块，负责将 IR 转换为目标机器的汇编代码或机器码，并进行优化和优化。</li><li>目标代码优化器：它是编译器的第六个模块，负责对生成的目标代码进行优化，以提高程序的执行效率和减少程序的存储空间。</li><li>目标代码链接器：它是编译器的最后一个模块，负责将多个目标文件和库文件链接成一个可执行文件，以及进行地址和符号重定位等操作。</li></ol><p>编译器的工作流程通常包括以下几个步骤：</p><ol><li>词法分析：将源代码中的字符序列转换为有意义的单词序列，并为每个单词分配 Token 种类。</li><li>语法分析：将 Token 序列转换为语法分析树或抽象语法树，以及对语法错误进行检测和报告。</li><li>语义分析：对语法分析树或抽象语法树进行类型检查、语义检查、符号表管理等操作，以及生成中间代码或目标代码。</li><li>中间代码生成：将 AST 或符号表转换为中间表示（IR），并进行优化和优化。</li><li>目标代码生成：将 IR 转换为目标机器的汇编代码或机器码，并进行优化和优化。</li><li>目标代码优化：对生成的目标代码进行优化，以提高程序的执行效率和减少程序的存储空间。</li><li>目标代码链接：将多个目标文件和库文件链接成一个可执行文件，并进行地址和符号重定位等操作。</li></ol><h3 id="遍"><a href="#遍" class="headerlink" title="遍"></a>遍</h3><p>扫描越多遍越不好</p><h3 id="文法"><a href="#文法" class="headerlink" title="文法"></a>文法</h3><p>ε 空串的意思，起到一个语义复写的作用，把属性复写成综合属性往上传</p><p>空集和空字符串不是一个东西</p><p>右箭头 产生式符号</p><p>右箭头加星号 推导</p><p>句型：从开始符号经过 N 步推导，之间生成的任何字符串都是句型</p><p>句子：相比句型，只有有终结符的，才是句子</p><p>0 型文法最简单、表达能力最强，1 型文法增加了约束条件，2、3 型增加了更多。约束条件越多，表达能力越弱。</p><h3 id="第三章"><a href="#第三章" class="headerlink" title="第三章"></a>第三章</h3><h4 id="NFA-DFA-的区别"><a href="#NFA-DFA-的区别" class="headerlink" title="NFA DFA 的区别"></a>NFA DFA 的区别</h4><ul><li>DFA 只有一个起点</li><li> NFA 上面是一个字符串，DFA 上面是一个字符</li><li> NFA 同一个状态出发，同一个条件可以到达不同的状态；DFA 只能到达同一个状态</li></ul><h4 id="改写、化简、状态矩阵"><a href="#改写、化简、状态矩阵" class="headerlink" title="改写、化简、状态矩阵"></a>改写、化简、状态矩阵</h4><blockquote><p>就是一开始学的图</p></blockquote><p>确定化</p><p>最少化</p><h4 id="正规式改写DFA"><a href="#正规式改写DFA" class="headerlink" title="正规式改写DFA"></a>正规式改写 DFA</h4><p>1(0|1)*101</p><h3 id="自上而下语法树构造"><a href="#自上而下语法树构造" class="headerlink" title="自上而下语法树构造"></a>自上而下语法树构造</h3><h4 id="左递归的消除"><a href="#左递归的消除" class="headerlink" title="左递归的消除"></a>左递归的消除</h4><p>E-&gt;Ex|Ey|z </p><p>把所有包含左递归的候选式提取出来 E -&gt;  E (x|y)|z</p><p>X=x|y Y=z</p><p>E-&gt;EX|Y</p><p>用通用格式改写</p><p>E-&gt;YE’</p><p>E’-&gt;XE’|e</p><p>再代入 X 和 Y</p><p>E-&gt;zE’</p><p>E’-&gt;xE’|yE’|e</p><p>直接左递归是指产生式中右边的第一个符号就是左边的符号，间接左递归则不是。</p><p>把间接左递归通过代入的方式，改写成直接左递归，再使用直接左递归通用的改写方式</p><h4 id="LL-1-1"><a href="#LL-1-1" class="headerlink" title="LL(1)"></a>LL(1)</h4><h5 id="意义"><a href="#意义" class="headerlink" title="意义"></a>意义</h5><p>第一个 L：从左往右输入，每次读入一个非终结符进行推导</p><p>第二个 L：产生式使用最左推导</p><p>1：每次只需要看右边的第一个数字</p><p>LL 代表左向右扫描输入，左推导，1 代表在任何时候，仅仅需要查看输入的下一个符号。</p><h5 id="三个条件"><a href="#三个条件" class="headerlink" title="三个条件"></a>三个条件</h5><p>不能存在左递归和二义性</p><p>可以通过 FIRST 集和 FOLLOW 集构造出一个 LL (1) 分析表</p><p>LL (1) 语法符合哪三个条件：不允许有左递归（改写），求 FIRST 集（从下往上，每个产生式的候选式相交为空、左边的符号能否推出 epsilon，如果一个产生式的候选式 L 推导出来之后能推出 epsilon，要拿它的 FOLLOW 集与每一个 FIRST 集相交）</p><p>如果一个文法的候选式可以推导出 ε，则 FIRST 和 FOLLOW 集都为空</p><h5 id="求FIRST集和FOLLOW集"><a href="#求FIRST集和FOLLOW集" class="headerlink" title="求FIRST集和FOLLOW集"></a>求 FIRST 集和 FOLLOW 集</h5><p>求 FOLLOW 集以右边为主</p><p>FOLLOW 集里是不可能有 epsilon 的，最多只有#</p><h2 id="最后一节新课（第七章）"><a href="#最后一节新课（第七章）" class="headerlink" title="最后一节新课（第七章）"></a>最后一节新课（第七章）</h2><h3 id="抽象语法树-AST"><a href="#抽象语法树-AST" class="headerlink" title="抽象语法树 (AST)"></a>抽象语法树 (AST)</h3><ul><li><p>操作符在中间，左右两边的子结点是两个数</p></li><li><p>在属性文法阶段生成抽象语法树</p></li><li><p>逆波兰式：使用后序遍历抽象语法树的结果</p></li><li><p>考试可能会考给一个逆波兰式，把抽象语法树写出来</p></li><li><p>AST 对编程是很重要的工具，因为操作符就是 “操作码”、子结点就是 “操作数”</p></li></ul><h3 id="DAG图"><a href="#DAG图" class="headerlink" title="DAG图"></a>DAG 图</h3><p>DAG 图是抽象语法树的简化表达形式</p><h3 id="三地址代码"><a href="#三地址代码" class="headerlink" title="三地址代码"></a>三地址代码</h3><blockquote><p>难度比较大，后面还有四元式、三元式、间接三元式，要注意区别。作用域及其之后的内容不考察。</p></blockquote><p>三地址代码是代码的逻辑表达形式，两个操作数加上一个运算符就是三地址了（如 a+b）</p><p>三地址语句是中间代码的一种抽象形式，四元式（操作码、两个操作数、操作结果）是三地址代码在内存中的一种存放形式。因此四元式表格中会有很多操作数是子树的操作结果（帮助自底向上运算）。</p><p>三元式相比四元式，省去了操作结果，把需要中间结果参与运算的地方替换成了运算这个中间结果的指令的编号（内存地址）。缺点：如果指令编号发生变化，则绑定操作结果的指令都出错了。为了解决这个缺点，有了间接三元式。</p><p>间接三元式的表格中的编号（1、2 等）指的并不是表格第一列的指令编号（内存地址），而是指向了间接码表中的间接代码，间接代码再去指向指令编号。需要调整运算顺序时，只需重新安排间接码表，无需改动三元式。</p><h2 id="期末复习课"><a href="#期末复习课" class="headerlink" title="期末复习课"></a>期末复习课</h2><h3 id="上下文无关文法"><a href="#上下文无关文法" class="headerlink" title="上下文无关文法"></a>上下文无关文法</h3><p>上下文无关文法（Context-Free Grammar, CFG）是一种形式文法，其产生式规则只能够在左侧非终结符号周围添加或删除终结符号，而不考虑符号周围的上下文信息。在上下文无关文法中，每个产生式规则都由一个非终结符号和一个由终结符号和非终结符号构成的字符串组成，用符号 “-&gt;” 分隔。</p><p>最左推导：每次都从产生式最多边第一个符号开始推导</p><p>语法的二义性：二义性文法是指存在两种或多种不同的解析方式，即存在多个推导树的上下文无关文法。这种情况会导致语法分析器无法准确地确定语法结构，进而产生歧义。</p><p>为了避免二义性，可以采取以下几种方法：</p><ol><li>优先级和结合性：在产生式规则中添加优先级和结合性的限制，例如在上述文法中，可以修改为 <code>E -&gt; E * E | num</code> 和 <code>E -&gt; E + E | num</code>，从而明确乘法优先于加法。</li><li>左递归消除：将产生式规则中的左递归转化为右递归，例如将 <code>E -&gt; E + E</code> 转化为 <code>E -&gt; E + T</code> 和 <code>T -&gt; E</code>，从而消除左递归，减少二义性。</li><li>增加限制条件：在文法中增加限制条件，例如在上述文法中，可以限制加号和乘号不能同时出现，从而避免二义性。</li></ol><h3 id="第三章-1"><a href="#第三章-1" class="headerlink" title="第三章"></a>第三章</h3><h4 id="词法分析"><a href="#词法分析" class="headerlink" title="词法分析"></a>词法分析</h4><p>词法分析的目标</p><p>结果（五类词）：标识符，关键字，阶符，，分隔符，常数</p><p>正则表达式（正规式）：正则闭包和正法闭包的区别</p><p>正规式改写为 NFA DFA DFA 化简 必考</p><p>NFA 和 DFA 的三个区别：</p><ol><li>状态转换规则不同：DFA 中，每个状态只能由当前输入的字符转换到唯一的一个状态，即对于每个状态和输入符号，只有唯一的下一个状态；而在 NFA 中，对于每个状态和输入符号，可以转移到零个或多个状态。</li><li>状态转移表的大小不同：由于 DFA 状态转移规则的唯一性，DFA 可以使用状态转移表来表示状态转换；而 NFA 由于其状态转换规则的非确定性，需要使用更复杂的转换关系来表示，通常采用状态转移图来表示，因此状态转移表的大小也不同。</li><li>接受输入的方式不同：DFA 接受输入的方式是在最终状态上标记一个可接受状态，即只有当输入字符串结束时，DFA 最终状态是可接受状态时，DFA 才能接受该输入；而在 NFA 中，可以有多个可接受状态，只要有一个状态是可接受状态，NFA 就可以接受输入字符串。</li></ol><p>总之，DFA 具有确定性、状态转移表简单、只有一个接受状态等特点，适合处理结构相对简单的输入；而 NFA 具有非确定性、状态转移图复杂、可有多个接受状态等特点，适合处理结构相对复杂的输入。</p><h3 id="第四章"><a href="#第四章" class="headerlink" title="第四章"></a>第四章</h3><h4 id="语法分析"><a href="#语法分析" class="headerlink" title="语法分析"></a>语法分析</h4><h5 id="从上往下推导和从下往上推导"><a href="#从上往下推导和从下往上推导" class="headerlink" title="从上往下推导和从下往上推导"></a>从上往下推导和从下往上推导</h5><p>从上往下推导是一种自顶向下的分析方法，它从语法的起始符号开始，通过不断地将它展开为更小的非终结符号，直到最终生成语法的终结符号序列。这个过程可以看作是一种自上而下的递归过程，常用的方法有递归下降分析（Recursive Descent Parsing）和预测分析（Predictive Parsing）。从上往下推导的一个特点是它需要提前知道语法的结构，也就是需要一个预测的语法分析表或递归下降分析程序。</p><p>从下往上推导是一种自底向上的分析方法，它从输入符号开始，逐步合并符号直到生成起始符号。这个过程可以看作是一种自底向上的规约过程，常用的方法有移进 - 归约分析（Shift-Reduce Parsing）和 LR 分析（LR Parsing）。从下往上推导的一个特点是它不需要提前知道语法的结构，而是通过观察输入符号和栈中的符号来进行规约操作，直到最终生成起始符号。</p><p>从上往下推导和从下往上推导在语法分析过程中的顺序和方式上存在一些区别。从上往下推导是自顶向下的展开过程，它从起始符号开始逐步展开直到终结符号；而从下往上推导是自底向上的合并过程，它从终结符号开始逐步合并直到起始符号。此外，从上往下推导需要预先知道语法的结构，而从下往上推导则是根据输入符号和栈中符号进行动态的规约操作。</p><h5 id="左递归改写"><a href="#左递归改写" class="headerlink" title="左递归改写"></a>左递归改写</h5><h5 id="从上往下推导时如何避免回溯"><a href="#从上往下推导时如何避免回溯" class="headerlink" title="从上往下推导时如何避免回溯"></a>从上往下推导时如何避免回溯</h5><ol><li>预测分析表（Predictive Parsing Table）：使用预测分析表可以在每个推导步骤中直接确定要选择的产生式，而无需回溯。预测分析表是一个二维表，行代表非终结符号，列代表终结符号（包括一个特殊的 “结束符号”），表格中的每个单元格包含相应非终结符和终结符组合的产生式。预测分析表的构建需要使用文法的 FIRST 和 FOLLOW 集合，确保表中没有冲突的项。</li><li>递归下降分析（Recursive Descent Parsing）：递归下降分析是一种基于递归的从上往下推导方法，每个非终结符号对应一个递归子程序。通过编写递归子程序来递归地展开非终结符号，遇到终结符号时进行匹配，避免回溯。确保在递归子程序中按照正确的顺序调用子程序，避免出现回溯。</li><li>预测子程序（Predictive Subroutine）：预测子程序是一种将递归下降分析与预测分析表相结合的方法。通过使用递归子程序和预测分析表中的产生式，可以在每个推导步骤中直接选择正确的产生式，避免回溯。</li><li>提前处理可选项：在编写文法时，可以将可选项提前处理为多个产生式，而不是使用可选项标记。这样可以减少回溯的机会，使得在每个推导步骤中能够直接选择正确的产生式。</li><li>语法设计的优化：通过优化文法的设计，可以减少回溯的可能性。例如，避免使用左递归、消除公共前缀等。</li></ol><h5 id="FIRST集的求法"><a href="#FIRST集的求法" class="headerlink" title="FIRST集的求法"></a>FIRST 集的求法</h5><h5 id="书上LL-1-模型：指针扫描字符串（看看）"><a href="#书上LL-1-模型：指针扫描字符串（看看）" class="headerlink" title="书上LL(1)模型：指针扫描字符串（看看）"></a>书上 LL (1) 模型：指针扫描字符串（看看）</h5><h3 id="第五章"><a href="#第五章" class="headerlink" title="第五章"></a>第五章</h3><h4 id="LR-0-1"><a href="#LR-0-1" class="headerlink" title="LR(0)"></a>LR(0)</h4><p>LR (0)：使用状态机来模拟堆栈 (LR 分析法，它的模型时什么？根据 LR 的分析表来判断四个动作，ACCEPT、空白出错等。规约（必会）：栈顶形成句柄时要将句柄弹出来，将栈顶的字符规约成一个符号，此时需要查 GOTO 表，这决定了新的字符进入新的栈顶之后，新的字符是什么字段) - r 规约 s 移进 accept:1， 空白：出错</p><p>移进和规约冲突等，LR (0) 就不行了</p><h4 id="SLR"><a href="#SLR" class="headerlink" title="SLR"></a>SLR</h4><p>规约与…</p><h4 id="LR-1-1"><a href="#LR-1-1" class="headerlink" title="LR(1)"></a>LR(1)</h4><p>推导项目集（作业里有，考试时没有那么难）</p><h4 id="LALR"><a href="#LALR" class="headerlink" title="LALR"></a>LALR</h4><p>LR (1) 会生成大量的项目集，爆炸式增长</p><h3 id="第六章"><a href="#第六章" class="headerlink" title="第六章"></a>第六章</h3><p>S 属性、L 属性、为什么要定义 L 属性</p><p>L 属性的引入是为了处理依赖于上下文信息的语法结构和语义规则。它允许从产生式的右部符号向左部符号传递属性值，以满足上下文相关的计算需求。在某些语法结构中，仅使用 S 属性无法满足对上下文信息的处理，而 L 属性可以提供更灵活的属性传递机制，支持对上下文信息的分析和计算。</p><h3 id="第七章"><a href="#第七章" class="headerlink" title="第七章"></a>第七章</h3><p>四个表达式（逆波兰式等）</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;不讲不考察的内容&quot;&gt;&lt;a href=&quot;#不讲不考察的内容&quot; class=&quot;headerlink&quot; title=&quot;不讲不考察的内容&quot;&gt;&lt;/a&gt;不讲不考察的内容&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://zhangt.top/CS</summary>
      
    
    
    
    <category term="学习" scheme="https://www.pil0txia.com/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="本科CS课程笔记" scheme="https://www.pil0txia.com/tags/%E6%9C%AC%E7%A7%91CS%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大三下" scheme="https://www.pil0txia.com/tags/%E5%A4%A7%E4%B8%89%E4%B8%8B/"/>
    
    <category term="编译原理" scheme="https://www.pil0txia.com/tags/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>【联想实习笔记】软件开发的流程和规范</title>
    <link href="https://www.pil0txia.com/post/2023-05-05_dev-processes-and-specifications/"/>
    <id>https://www.pil0txia.com/post/2023-05-05_dev-processes-and-specifications/</id>
    <published>2023-05-05T15:03:43.000Z</published>
    <updated>2023-06-01T08:10:40.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>持续更新中</p></blockquote><h2 id="入职前他人的指导"><a href="#入职前他人的指导" class="headerlink" title="入职前他人的指导"></a>入职前他人的指导</h2><blockquote><p><a href="https://www.v2ex.com/t/935603">24 届实习，求问联想 Java 开发和奇安信 Go 开发选哪个？ - V2EX</a></p><p>ChaYedan666 #53</p></blockquote><p>“我看了你的简历，如果简历没有夸大的话，你的水平是能够上手初级开发来承接需求的，对于应届生来说，你的积累已经够了，是很优秀的，去实习的时候，一定要去联想或者大厂的正常部门跟着走一遍接需求的流程，学怎么工作，你现在需要的我认为不是技术方面的问题，因为开发不仅是技术，你需要跟很多人沟通，对齐，怎么 cover 住你负责的需求。</p><p>作为一个前年毕业的应届生，跟你一样经历过很卷的面试和迷茫期，这个迷茫是因为目前的校招的招聘难度已经远远大于了应届生匹配的水平，所以我知道你现在是在怀疑人生，感觉自己什么都会一点，但一旦让你自己上手写，可能就有点抓瞎，但你需要记住，你现在是大三，是大学生，是一个普通人，这是很正常的情况，你不需要再在这上面想太多，我认为对于你现阶段的目前很明确：</p><ol><li><p>去实习，了解职场中软件开发的流程和规范，怎么和需求方，PM ，上下游 RD ，QA 沟通和掌握整体的开发节奏。</p></li><li><p>对于实习中，你会参与到系统很小一部分的改动，你会阅读到大量的代码，遇到不懂的，多去与这个系统的人交流，因为系统是为了实现业务需求，你需要了解业务流程，才能知道为什么代码这么写</p></li><li><p>因为七月秋招应该就会开始了，所以你首先把表达你实习期间掌握的业务流程和你对于业务的理解和自己做需求时遇到的难点用文字清晰的表达在简历上，正常面试的话，面试官肯定会根据你简历问；下班时间，写你的简历，根据你的简历你思考会有什么面试题，例如你简历上写了 Java ，那么 Java 的基本知识的面试题你就得多看看，写了 mysql ，mysql 的面试题什么索引啊，慢查啊，怎么避免慢查这样的面试题多看看，然后还有重要的算法题，你应该下班时间每天至少上去写一两题</p></li><li><p>说会到个人时间的自学，高效自学就得上手写代码，我的意思是，在你个人时间里，实现自己的需求学习新知识，然后运用到工作中；我举个例子，例如你现在不会 kafka ，现在你又想看电子游戏相关的新闻；那么你自己写两个模块，用上 kafka 来进行学习；其中一个模块就是爬游戏网站例如 3DM 的游戏新闻页面，然后按照单个游戏维度，把新闻做聚合，然后发送 kafka 消息；另一个模块，就是消费你发送的消息，展现给用户（你自己）；这是个很明显的伪需求，只是你自己为了学习 kafka 而做的。但因为你自己实现了自己的伪需求，你就基本掌握了 kafka 的使用；如果在这之后，公司有新项目，新需求，需要使用到 kafka ，因为你在实现上述伪需求的时候已经掌握了，所以对你来说，工作是一件比较轻松的事情，但工作需要考虑到 kafka 的更多使用场景，例如稳定性，消费速率，消费失败重试等等情况，但你已经掌握了基本知识，就算遇到不懂的，也会学的非常快。”</p></li></ol><blockquote><p>Telegram a q</p></blockquote><p>“哈哈，联想直接叫 x 老师就行，大部分时间都是 teams 沟通，见不到人的</p><p>一般需求都是在 Confluence 上写清楚的，各个团队都有知识库，让你 leader 给你开权限就能看</p><p>另外联想和互联网企业架构有点不一样</p><p>都是平台给需求，中间有个 DT 岗拆解需求，然后给 PA 继续拆解，BA 来做原型逻辑设计，然后是 IT 团队写代码，代码完了再内测，UAT，安全审查等等，一套流程下来一个大需求差不多也得几个月上线哈哈</p><p>另外开发都得会 jira，一般都是在 jira 上沟通问题”</p><h2 id="岗位简写解释"><a href="#岗位简写解释" class="headerlink" title="岗位简写解释"></a>岗位简写解释</h2><p>PM: Product &amp; Marketing 产品经理</p><p>RD: Research and Development Engineer 研发工程师。对某种不存在的事物进行系统的研究和开发并具有一定经验的专业工作者，或者对已经存在的事物进行改进以达到优化目的的专业工作者。</p><p>QA: Quality Assurance Engineer</p><p>DT: <del>Data Technology Business Process Design “根据业务情况梳理客户 LTV 经营业务流程，挖掘客户经营机会和提升点；规划客户 LTV 经营产品平台，包括客户数据分析的数据产品规划和面向运营和营销的平台工具规划”</del>Digital Transformation 数字化转型，通常由 Data Analyst 数据分析岗位负责。</p><p>PA: Project Administrator</p><p>BA: Business Analyst 商务分析师。Business Analytic 和 Business Analysis 有点不一样，一个偏数据分析，一个偏业务和需求分析。（天津 Site 没有此岗位）</p><h2 id="需求处理流程"><a href="#需求处理流程" class="headerlink" title="需求处理流程"></a>需求处理流程</h2><p>原型是由 DT 同事和 UI/UE 同事给出来的</p><p>BSR: Business Solution Review。业务沟通阶段，一般发生在需求一开始，DT 会和业务沟通看下都是要做什么，这个给出来的文档就是和业务沟通的结果。</p><p>ISR: It Solution Review。基于 BSR 和 IT 同事沟通解决方案的文档，比如说涉及到哪些流程，和哪些系统交互，会用到什么东西。</p><p>PRD: Product Requirement Document/Product Requirement Definition</p><p>FS: Function Spec。一个比较粗的需求文档，它包含了基本的功能，但是有一些细小的逻辑可能并没有写出来，因此还会有 PRD。有的 DT 会在 FS 中描写的就比较详细，因此并不需要 PRD 文档。</p><p>DEV: Development</p><p>SIT: System Integration Test 系统整合测试</p><p>UAT: User Acceptance Test 用户验收测试</p><p>PRE: 生产环境前预部署</p><p>PRD: 生产环境上线</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;持续更新中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;入职前他人的指导&quot;&gt;&lt;a href=&quot;#入职前他人的指导&quot; class=&quot;headerlink&quot; title=&quot;入职前他人的指导&quot;&gt;&lt;/a&gt;入职前他人的指导&lt;/h2&gt;&lt;blockquo</summary>
      
    
    
    
    <category term="Java" scheme="https://www.pil0txia.com/categories/Java/"/>
    
    
    <category term="实习" scheme="https://www.pil0txia.com/tags/%E5%AE%9E%E4%B9%A0/"/>
    
    <category term="流程" scheme="https://www.pil0txia.com/tags/%E6%B5%81%E7%A8%8B/"/>
    
    <category term="规范" scheme="https://www.pil0txia.com/tags/%E8%A7%84%E8%8C%83/"/>
    
  </entry>
  
  <entry>
    <title>《你的名字。》 2K SDR 4K 原盘超采样壁纸</title>
    <link href="https://www.pil0txia.com/post/2023-04-05_kimi-no-na-wa-wallpaper/"/>
    <id>https://www.pil0txia.com/post/2023-04-05_kimi-no-na-wa-wallpaper/</id>
    <published>2023-04-05T14:42:16.000Z</published>
    <updated>2023-04-05T14:42:16.000Z</updated>
    
    <content type="html"><![CDATA[<p>受<a href="https://www.pil0txia.com/post/2020-10-01_tenki-no-ko-4k-hdr-wallpaper/">上一篇《天气之子》壁纸</a>中 <code>3ick</code> 的邀请，正值<a href="../2023-04-05_suzume-no-tojimari-review/">《铃芽之旅》新作上映</a>，二刷完的我也借机回忆了一下五年前的自己是以怎样的标准去截取 “足以作为电脑桌面壁纸的分镜” 的。希望正在回顾《你的名字。》（『 君の名は。』, Kimi no na wa, Your Name) 的你也能重拾那种感动。</p><p>以下图片经过了 <code>Webp</code> 压缩，经过参数调优，网页浏览的观感没有区别，只有在 150% 放大后才能看出细节的损失。</p><p>你可以从 <a href="https://www.aliyundrive.com/s/Ygg54fGiHDv">阿里云盘</a> (提取码: <code>c71y</code>) 或 <a href="https://terabox.com/s/1xm4rx5bu5AI8fQduLJFg2Q">TeraBox</a> (提取码: <code>qs7d</code>) 打包下载未压缩原图。</p><p><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-22374219.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-22384657.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-22441169.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-22504085.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-22531181.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-23031515.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00124-23042117.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12305355.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12343926.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12345511.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12354561.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12365070.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12372363.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12380334.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12390364.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12395735.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12404474.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12405097.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12411363.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12413049.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12413876.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12415312.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12441558.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12462227.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12583349.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-12584551.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13161374.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13175427.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13313064.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13314189.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13315016.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13343431.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13355407.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13571474.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13585938.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-13593319.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14032062.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14034904.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14041605.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14043348.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14051694.webp"><br><img src="https://vc.static.pil0txia.com/gallery/kimi_no_na_wa/S00125-14065436.webp"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;受&lt;a href=&quot;https://www.pil0txia.com/post/2020-10-01_tenki-no-ko-4k-hdr-wallpaper/&quot;&gt;上一篇《天气之子》壁纸&lt;/a&gt;中 &lt;code&gt;3ick&lt;/code&gt; 的邀请，正值&lt;a href=&quot;../20</summary>
      
    
    
    
    <category term="ACG" scheme="https://www.pil0txia.com/categories/ACG/"/>
    
    
    <category term="壁纸" scheme="https://www.pil0txia.com/tags/%E5%A3%81%E7%BA%B8/"/>
    
    <category term="新海诚" scheme="https://www.pil0txia.com/tags/%E6%96%B0%E6%B5%B7%E8%AF%9A/"/>
    
    <category term="你的名字。" scheme="https://www.pil0txia.com/tags/%E4%BD%A0%E7%9A%84%E5%90%8D%E5%AD%97%E3%80%82/"/>
    
    <category term="2K" scheme="https://www.pil0txia.com/tags/2K/"/>
    
    <category term="超采样" scheme="https://www.pil0txia.com/tags/%E8%B6%85%E9%87%87%E6%A0%B7/"/>
    
  </entry>
  
  <entry>
    <title>《铃芽之旅》是新海诚对粉丝的背叛吗？不，是他对自己的忠实。</title>
    <link href="https://www.pil0txia.com/post/2023-04-05_suzume-no-tojimari-review/"/>
    <id>https://www.pil0txia.com/post/2023-04-05_suzume-no-tojimari-review/</id>
    <published>2023-04-05T13:49:46.000Z</published>
    <updated>2024-02-29T10:32:46.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>2024-02-29 更新：<a href="https://www.pil0txia.com/post/2024-02-29_suzume-no-tojimari-wallpaper/">《铃芽之旅》4K SDR madVR 渲染壁纸</a></p></blockquote><h2 id="3月25日激光厅初见感想"><a href="#3月25日激光厅初见感想" class="headerlink" title="3月25日激光厅初见感想"></a>3 月 25 日激光厅初见感想</h2><p>很开心，这是一部我等了三年的电影，能和伴侣一起看，就像是完成了一个约定一样</p><p>铃芽之旅的爱情线，我觉得太完美了，过于圆满。应该多次强调男主与结冰的对抗与逐渐陷入弱势，但尽管如此，男主都非常亚撒西地呵护女主，女主也表现出自己少女的小心思和可爱之处。然后两人在一次闭门的时候失败了，地震让很多人死去，两人陷入了深深的自责之中。此时两人被神秘的白猫一路溜着来到了一个关键的地方，领悟了男主必须变成要石的命运，女主和男主陷入深深的难舍难分与痛苦之中，最终男主决定守护更多人的生活，恳求女主将自己插入蚓厄。</p><p>后来女主彻夜难眠辗转之中再次踏上拯救男主的道路，最终虽然男主从山顶被拔下，然而常世也掀起了一场巨大的风暴，两人在风暴中被卷向不同的方向，但是在黑猫和白猫的守护下分别从两扇往门离开了常世，影片以两人踏上寻找另一半的旅途收尾。</p><p>只有这样，才能让人知道你看的是新海诚的电影。</p><p>其实这并不是一部爱情电影，而是新海诚对自己《追逐繁星的孩子》的补全与重绘。但我还是期待诚哥能在下一部三年之约继续给我带来渺小的少男少女就算是迎着距离感也要克服重重困难相见的世界系作品。</p><h2 id="4月2日杜比影院二刷感想"><a href="#4月2日杜比影院二刷感想" class="headerlink" title="4月2日杜比影院二刷感想"></a>4 月 2 日杜比影院二刷感想</h2><p>尽管已经看过解说与细节设定，但耳听为虚，眼见为实，只有彻彻底底的视听体验释放，才能验证我对这部电影的最终评价。</p><p>事实证明，我没有选错，无论是凌晨四点半抢票也好，还是稍远一些的路程也好，亦或是对二刷自身的认知和对额外付费的评估，当我真正坐下来躺在皮质座椅上开始欣赏映前秀时，一切都烟飘云散了。这是我有史以来听过的最好的音响系统，它坚定了我以后要在家中组装一套 5.1 甚至 7.1 家庭影院的愿望。</p><p>这种视听的完美演绎让我从第一次观影的平凡体验中完全脱离，以往爆发力不足或超限得令人尴尬的片段，在这里是如此的恰到好处，给我的心情一记猛击，随之而来的是一阵阵哽咽 —— 玲芽洗去淤伤、踏上寻找草太之路时响起的 lululu 变奏；找到往门时受到夸奖而重回扑闪扑闪状态的大臣说的 “一起去吧（行こう）”；玲芽被常世的天空照亮的侧脸扬着自信的笑容说出的 “去我喜欢的人那里！”，不顾一切穿越火海抵达草太身边、哪怕成为要石也要带回草太、并最终随着草太被拽起身而碎裂脱落的冰块声；幼年玲芽误入常世时熟练得令人心疼的寻母哭诉…… 这些都是令我眼角湿润、并流下泪珠的演绎，这种体验，是第一次所未曾有的。每当我胸口颤动时，她握着的我的手都会轻轻安抚。我以为这是她的从容，直到她抬起眼镜用纸巾擦拭眼角。我宁愿把泪珠留在眼眶下，干涸在那里是一种纪念。</p><p>我再次重申，这不是一部爱情片，如果从一张白纸去看它，而不是寄予你的名字的期望，你反而会从它的爱情线中获取更多惊喜。在理解其他线叙事的铺垫深度之后，曾经这么薄弱的爱情线反而更令人动容，至少我和她在二刷后内心爱的甜蜜感是要更多的。玲芽的行动动机并不是爱情，而是对自己 “害死” 了草太的愧疚；玲芽两次喊出我不怕死的原因，也是自己作为灾难幸存者 “为什么死的不是我，为什么死的是他” 的愧疚；玲芽大声喊出 “没有草太的世界我很害怕” 的基础，也变得明了了。虽然新海诚可能觉得还是欠缺火候，于是用爷爷的大笑打破了这里的气氛。</p><p>二刷带给我的情绪化的全新认知，才是支撑理性判断和分析推理的重要支柱。毕竟，你这么说，有道理，可我自己看电影时这么想，才是属于自己的真实。这完全推翻了我第一次看后预想的 “新海诚式” 剧情走向，后者大大削弱了影片的表意，或者说，倒不如说我的设想才是对商业化的妥协，这部作品才是新海诚对自己《追逐繁星的孩子》的延续。</p><p>关一次门就爽一次，这句话是真的。就算没来得及吃东西肚子饿了，也要好好看完关门再掏零食。娇在那埋怨我影响到右边的人了，但她自己却饿着肚子看完了电影，刚亮灯就掏出手机开始选饭馆，也没有和我一起合唱一下，有点可惜呢，心疼她。她上次也是，明明边上的人一直在讲话，她也没有勇气去打扰别人。但亮灯后我看她也很开心，既然你也喜欢，我也没什么意见。</p><p>最后，电影的两首主题曲，我已经快会唱了，毕竟从片尾曲插入起就对着口型摇头晃脑真的很有沉浸感。</p><p><img src="https://static.pil0txia.com/gallery/mylife/VID_20230402_155109.mp4_000007.530.webp" alt="VID_20230402_155109.mp4_000007.530"></p><p>今天的阳光下墨镜的照片和在影院最后录的视频，发给了好友。某高中同学说 “好帅，磕到了，这是手机拍的吗”，某大学同学说 “羡慕啊，感觉你去哪都带着女朋友，现充”。原来自己早就已经是现充了啊，但还能保持着对 Anime 的热爱，还能秉持着五年前的少年心去欣赏新海诚的作品，真是一件美好的事情。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;2024-02-29 更新：&lt;a href=&quot;https://www.pil0txia.com/post/2024-02-29_suzume-no-tojimari-wallpaper/&quot;&gt;《铃芽之旅》4K SDR madVR 渲染壁纸&lt;/a&gt;&lt;/</summary>
      
    
    
    
    <category term="ACG" scheme="https://www.pil0txia.com/categories/ACG/"/>
    
    
    <category term="新海诚" scheme="https://www.pil0txia.com/tags/%E6%96%B0%E6%B5%B7%E8%AF%9A/"/>
    
    <category term="铃芽之旅" scheme="https://www.pil0txia.com/tags/%E9%93%83%E8%8A%BD%E4%B9%8B%E6%97%85/"/>
    
    <category term="影评" scheme="https://www.pil0txia.com/tags/%E5%BD%B1%E8%AF%84/"/>
    
    <category term="情侣" scheme="https://www.pil0txia.com/tags/%E6%83%85%E4%BE%A3/"/>
    
  </entry>
  
  <entry>
    <title>www 域名前缀过时了吗？你需要知道的一切</title>
    <link href="https://www.pil0txia.com/post/2023-01-17_www-is-outdated-or-not/"/>
    <id>https://www.pil0txia.com/post/2023-01-17_www-is-outdated-or-not/</id>
    <published>2023-01-17T10:57:10.000Z</published>
    <updated>2023-01-17T14:16:30.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>我敢说这绝对是最适合正在纠结要不要给博客网址加上 www 的你阅读的文章，因为我已经替你纠结过了。让我们以一个从 <code>example.com</code> 迁移到 <code>www.example.com</code> 的具体案例来为你解释前者与后者在 DNS 性能、CDN 覆盖和业务管理上的区别。</p><h2 id="裸域：DNS-解析的天生逆境"><a href="#裸域：DNS-解析的天生逆境" class="headerlink" title="裸域：DNS 解析的天生逆境"></a>裸域：DNS 解析的天生逆境</h2><p>以前我用 Netlify 托管站点的时候，主站的裸域名 pil0txia.com 默认 CNAME 解析在 Netlify 的负载均衡器上，请求会先访问 <code>apex-loadbalancer.netlify.com</code>，然后返回一个相对较近的 CDN 节点的 IP 地址。现在看来其实是绕了远路的，这也是后来 <a href="https://docs.netlify.com/domains-https/custom-domains/configure-external-dns/#configure-an-apex-domain">Netlify 文档</a> 中补充 <code>这意味着裸域名不能利用全球CDN上的直接DNS路由，我们建议在使用外部DNS时为你的主域使用一个子域</code> 的原因。</p><p>而且，按照 RFC 规范，@ 解析不支持 CNAME 记录，否则会和 MX 记录<a href="https://docs.dnspod.cn/dns/dns-resolve-set/">冲突</a>，导致域名邮箱收不到邮件。但我以前用了很久这样的配置，邮箱也没有丢件，是因为 DNSPod 对 @ 解析的 CNAME 记录默认启用了 <a href="https://docs.dnspod.cn/dns/cname-speed/">CNAME 加速</a>，类似于 Cloudflare 使用的 <a href="https://blog.cloudflare.com/introducing-cname-flattening-rfc-compliant-cnames-at-a-domains-root/">CNAME Flattening</a> 技术，由 DNSPod 服务器代替用户递归解析并直接返回 A 记录，避免了冲突。</p><h2 id="裸域：CDN-覆盖的如获新生"><a href="#裸域：CDN-覆盖的如获新生" class="headerlink" title="裸域：CDN 覆盖的如获新生"></a>裸域：CDN 覆盖的如获新生</h2><p>不过 DNSPod 不像 Cloudflare 有很多数据中心、可以解析出离用户最近的 CDN 节点，DNSPod 免费版只有上海、南京两个区域的 DNS 集群，很有可能无法做到分地区分运营商解析，使 CDN 的优势损失。后来我为了用国内的 CDN 节点，专门跟客服求证了这一点：</p><blockquote><p>Q:</p><p>DNSPod 给在腾讯云购买的域名解析 DNS 时，能否支持 edns_client_subnet (ECS) 协议，也就是说，在没有手动配置线路的情况下，自动根据用户的位置，返回更近的解析？</p><p>良好支持就是细分地域解析（CDN 的节点粒度），反之就是没那么细（DNS 节点的粒度），甚至产生偏差（因为免费版和第一个付费版的境内节点都在上海和南京）。</p></blockquote><blockquote><p>A:</p><p>CDN 也是托管在我们 DNSPod，我让后台确认下，是否有加速，是否影响解析精度。</p><p>您好，经后台确认，腾讯云 CDN 的域名是用的 DNSPod 解析，并且要开启 CNAME 加速。如果是 DNSPod 解析就支持开启 CNAME 加速，对 CDN 本身没有影响，还是根据客户端的 localdns 请求过来做返回。</p><p>加速时间是，不用客户端 DNS 再次请求到 DNSPod，一次请求就返回了 IP。不是 DNSPod 解析 的 CDN 就不支持 CNAME 加速。</p></blockquote><p>也就是说，由 DNSPod 负责解析的 CDN（如腾讯云和多吉云），DNSPod 储存了 CDN 不同节点的地理位置与线路，可以根据用户透传过来的 IP 位置做就近解析。</p><p>但如果不是由 DNSPod 负责解析的 CDN，应该使用传统方式，依靠 CDN 解析的域名来实现这一点。如果此时开启 CNAME 加速，就会由 DNSPod 错误地代替用户查询了并不是最优的 IP 地址。</p><h2 id="www：是遗老还是经典"><a href="#www：是遗老还是经典" class="headerlink" title="www：是遗老还是经典"></a>www：是遗老还是经典</h2><p>出生在互联网时代的 Blogger 们：在 WWW 还在被称作万维网的时候，作为一个 IT 管理员，你的网络系统就是你的域，比如  <code>example.com</code>。域的下面是主机，每个联网的设备都可以算作一个主机。</p><p>在你的域内，为网络页面提供服务的主机通常被加上 “www” 前缀，它的主机名也就是  <code>www.example.com</code>。在互联网早期阶段，还没有虚拟主机和托管平台这回事。 所有网页服务器的主机名都要指向一个 IP 地址，只有在裸域需要迁移时才指向另一个网址。</p><p>互联网普及开后，一些管理员开始把裸域名指向和网页服务器主机同样的 IP 地址，访客只需在浏览器中输入 <code>example.com</code> 即可访问，而不用输入完整的 <code>www.example.com</code>。我喜欢这样，这很简洁，这也是我一开始采用 Netlify 默认设置的原因。</p><p>但 <code>example.com</code> 和 <code>www.example.com</code> 对搜索引擎而言意味着两个相同的网站。为了搜索引擎优化 (Search Engine Optimization,SEO)，必须选择一个规范的名称，而且另一个也要使用 HTTP 301 响应码来指向这个规范的主机名。</p><p>对 SEO 而言，选哪个都一样。但是从业务的实践上来说，就有很大区别了。首先，我们上面讲了 DNS 的性能问题和 CDN 的覆盖问题，这让我考证了好一段时间，因为我一开始并不信任没有被写在规范中的基础设施新技术。那么我们现在知道了，至少在 Cloudflare 或腾讯云这两家厂商的封闭体系中，我们可以享受到给裸域使用 CDN 加速的全部优势，但谁知道我会不会有一天转向别的厂商呢？</p><p>此外，虽然我们都知道饼干 (cookie) 是用来吃的，但它如果被设置在 <code>example.com</code> 裸域上，它的 domain 属性（也就是作用域）将会使它被发送到所有的子域名上，包括 <code>mail.example.com</code> 或 <code>pay.example.com</code> 等你并不想看见它的地方。为了避免这种情况，开发时必须不显式地指定 domain，浏览器就只会生成一个针对当前域名的 cookie，不包含子域名。</p><p>如果我正在为你的网站进行安全审计，目前只有 <code>mail.example.com</code> 的权限，而你把 CMS 部署在了 <code>example.com</code> 上，那我一定会试着读取一下 CMS 给你的浏览器发送的 cookie，说不定就能以你的名义登录 CMS 的管理后台。</p><h2 id="我为什么要迁移到www"><a href="#我为什么要迁移到www" class="headerlink" title="我为什么要迁移到www"></a>我为什么要迁移到 www</h2><p>说真的，如果你只是一个人写写给自己看的文章，随你的喜好吧。也许是我看了两年的 <code>pil0txia.com</code> 有点腻了，也可能是我在电脑前忙活了一整天感到了厌烦，反正我睡了一觉第二天醒来后就觉得 <code>pil0txia</code> 的字母 <code>p</code> 的下摆太长了，也许用 <code>www.pil0txia.com</code> 来平衡一下左侧的重量也蛮好的。</p><h2 id="迁移之后不能忘记的事"><a href="#迁移之后不能忘记的事" class="headerlink" title="迁移之后不能忘记的事"></a>迁移之后不能忘记的事</h2><p>作为一个跟静态页面和前端打交道的 Blogger，回到 WordPress 那种相对高昂的运营支出仿佛是一种背叛。但当我完成 ICP 备案后寻寻觅觅都找不到一个支持 301 跳转的服务商时，我忍不住地想要不要掏出自己 Azure 的云主机。是的，我知道想要把 @ 重定向到 www 只需要 302 跳转就行了，这用任何一家的 URL 重写都可以做到。但是看着谷歌搜索结果里我的索引混杂着 <code>https://pil0txia.com</code> 和 <code>https://www.pil0txia.com</code>，着实有一种在逛 V2EX 的感觉。</p><p>301 跳转会让谷歌知道我的站点地址发生了更改，然后慢慢地从搜索结果中把旧地址的流量转移到新地址上，并最终完全剔除旧的地址。无论是域名迁移还是站点迁移，这都是对 SEO 最友好的方式。然而在整个备案和迁移的过程中，唯一与我的预想不符的是，DNSPod 的显性 URL 转发并不支持 HTTPS 地址，因为它的转发服务器上没有我的 SSL 证书，也不支持设置。</p><p>还好我还记得七牛云的 OSS 对象存储可以 301 跳转。对于只有 CDN 的 HTTP 流量计入免费额度的七牛云来说，由存储桶直接吞吐 HTTPS 流量是会计费的，但这通常达不到计费的最小精度 (0.01)。事实上，四年来，我体验了所有云服务厂商几乎所有的产品，还从来没有在基础设施上花过一分钱。</p><h2 id="附言"><a href="#附言" class="headerlink" title="附言"></a>附言</h2><p>最后，我为什么要用 “裸域” 这么别扭的名字？因为它是英译中后的名词中唯一没有产生误解的。</p><p>让我澄清一下，顶级域或根域名 (Top-level Domain,TLD/root domain) 都指的是类似于<code>.com</code> 的 “后缀”，也被称为一级域名；<code>example.com</code> 在英文中被称为 <code>apex domain</code> 或 <code>bare domain</code>，但前者直译过来的 “顶域” 实在太容易与 TLD 混淆了，只有 “裸域” 还能直接称呼。</p><p>曾经口口相传的 “二级域名 (Second Level Domain,SLD) ” 也并不是 “子域名 (subdomain) ”，前者等同于裸域，而后者类似于 <code>www.example.com</code>，其实是三级域名。国内云服务厂商的人员培训根本没有做好这一点，往往不知所云。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;我敢说这绝对是最适合正在纠结要不要给博客网址加上 www 的你阅读的文章，因为我已经替你纠结过了。让我们以一个从 &lt;code&gt;example</summary>
      
    
    
    
    <category term="运维" scheme="https://www.pil0txia.com/categories/%E8%BF%90%E7%BB%B4/"/>
    
    <category term="网络运维" scheme="https://www.pil0txia.com/categories/%E8%BF%90%E7%BB%B4/%E7%BD%91%E7%BB%9C%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="迁移" scheme="https://www.pil0txia.com/tags/%E8%BF%81%E7%A7%BB/"/>
    
    <category term="网络运维" scheme="https://www.pil0txia.com/tags/%E7%BD%91%E7%BB%9C%E8%BF%90%E7%BB%B4/"/>
    
    <category term="CDN" scheme="https://www.pil0txia.com/tags/CDN/"/>
    
    <category term="域名" scheme="https://www.pil0txia.com/tags/%E5%9F%9F%E5%90%8D/"/>
    
    <category term="DNS" scheme="https://www.pil0txia.com/tags/DNS/"/>
    
    <category term="SEO" scheme="https://www.pil0txia.com/tags/SEO/"/>
    
    <category term="cookie" scheme="https://www.pil0txia.com/tags/cookie/"/>
    
  </entry>
  
  <entry>
    <title>基于不蒜子 2.3 自定义站点访问量 (site_pv, site_uv, page_pv)</title>
    <link href="https://www.pil0txia.com/post/2023-01-16_busuanzi-modified/"/>
    <id>https://www.pil0txia.com/post/2023-01-16_busuanzi-modified/</id>
    <published>2023-01-16T10:58:49.000Z</published>
    <updated>2023-01-16T16:58:49.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="busuanzi-modified"><a href="#busuanzi-modified" class="headerlink" title="busuanzi-modified"></a>busuanzi-modified</h1><p>基于不蒜子 2.3 官网数据自定义站点访问量 (site_pv, site_uv, page_pv)。Customize your site view count based on busuanzi.</p><p><a href="https://github.com/Pil0tXia/busuanzi-modified">https://github.com/Pil0tXia/busuanzi-modified</a></p><h2 id="修改"><a href="#修改" class="headerlink" title="修改"></a>修改</h2><p>你可以自定义 <code>busuanzi.pure.js</code> 中的第 75, 80, 85 行，分别对应了站点访问量、独立访客和文章阅读量。取一个你喜欢的数字，然后自行压缩并托管。</p><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>将你的网站从 <code>busuanzi.ibruce.info</code> 引用的 <code>busuanzi.pure.mini.js</code> 替换为新的 js 地址，如本仓库的 <code>https://raw.githubusercontent.com/Pil0tXia/busuanzi-modified/main/busuanzi.pure.mini.js</code></p><p>你也可以使用 CDN 加速访问：</p><ul><li><p>jsDelivr:&nbsp;<code>https://cdn.jsdelivr.net/gh/Pil0tXia/busuanzi-modified/busuanzi.pure.mini.js</code></p></li><li><p>渺软公益 CDN：<code>https://jsd.onmicrosoft.cn/gh/Pil0tXia/busuanzi-modified/busuanzi.pure.mini.js</code></p></li><li><p>我自用的 CDN（不作任何 SLA 承诺）：<code>https://static.pil0txia.com/assets/busuanzi/2.3/busuanzi.pure.mini.js</code></p></li></ul><p>如果你正在使用 Hexo 的 Butterfly 主题，请参考<a href="https://butterfly.js.org/posts/4aa8abbe/#%E8%A8%AA%E5%95%8F%E4%BA%BA%E6%95%B8-busuanzi-UV-%E5%92%8C-PV">这篇文档</a>修改地址。</p><h2 id="建议"><a href="#建议" class="headerlink" title="建议"></a>建议</h2><p>建议将其用来迁移站点历史访问量即可，例如从 @记录迁移到了 www，却丢失了以前的访问量。数字改得太大，SEO 指数却很低，会被人笑话的。这只是一个建议。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;busuanzi-modified&quot;&gt;&lt;a href=&quot;#busuanzi-modified&quot; class=&quot;headerlink&quot; title=&quot;busuanzi-modified&quot;&gt;&lt;/a&gt;busuanzi-modified&lt;/h1&gt;&lt;p&gt;基于不蒜子 2.3 </summary>
      
    
    
    
    <category term="前端" scheme="https://www.pil0txia.com/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
    <category term="JavaScript" scheme="https://www.pil0txia.com/tags/JavaScript/"/>
    
    <category term="不蒜子" scheme="https://www.pil0txia.com/tags/%E4%B8%8D%E8%92%9C%E5%AD%90/"/>
    
    <category term="前端" scheme="https://www.pil0txia.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="访问量" scheme="https://www.pil0txia.com/tags/%E8%AE%BF%E9%97%AE%E9%87%8F/"/>
    
    <category term="pv" scheme="https://www.pil0txia.com/tags/pv/"/>
    
    <category term="uv" scheme="https://www.pil0txia.com/tags/uv/"/>
    
    <category term="CDN" scheme="https://www.pil0txia.com/tags/CDN/"/>
    
  </entry>
  
  <entry>
    <title>Linux 后台程序的创建与管理</title>
    <link href="https://www.pil0txia.com/post/2022-12-11_linux-background-programs/"/>
    <id>https://www.pil0txia.com/post/2022-12-11_linux-background-programs/</id>
    <published>2022-12-10T17:46:39.000Z</published>
    <updated>2022-12-10T17:46:39.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>用 ssh 登录远程的 Linux 服务器，某些程序或脚本会在前台持续运行、占用 shell 窗口，且终端连接断开时任务也会中止。如何让运行命令提交后不受本地关闭终端窗口或网络断开连接的干扰呢？本文列举了一些实用的命令。</p></blockquote><h2 id="省流版"><a href="#省流版" class="headerlink" title="省流版"></a>省流版</h2><p><code>nohop &lt;command&gt; &amp;</code></p><h2 id="nohup"><a href="#nohup" class="headerlink" title="nohup"></a>nohup</h2><h3 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h3><p>如果只是临时有一个命令需要长时间运行，什么方法能最简便的保证它在后台稳定运行呢？</p><h3 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h3><p>我们知道，当用户注销 (logout) 或者网络断开时，终端会收到 HUP (hangup) 信号从而关闭其所有子进程。因此，我们的解决办法就有两种途径：要么让进程忽略 HUP 信号，要么让进程运行在新的会话里从而成为不属于此终端的子进程。</p><p>nohup 无疑是我们首先想到的办法。顾名思义，nohup 的用途就是让提交的命令忽略 hangup 信号。让我们先来看一下 nohup 的帮助信息：</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">NOHUP(1)                        User Commands                        NOHUP(1)</span><br><span class="line">NAME</span><br><span class="line">       nohup - run a command immune to hangups, with output to a non-tty</span><br><span class="line">SYNOPSIS</span><br><span class="line">       nohup COMMAND [ARG]...</span><br><span class="line">       nohup OPTION</span><br><span class="line">DESCRIPTION</span><br><span class="line">        Run COMMAND, ignoring hangup signals.</span><br><span class="line">       --help display this help and exit</span><br><span class="line">       --version</span><br><span class="line">              output version information and exit</span><br></pre></td></tr></tbody></table></figure><p>nohup 的使用只需在要处理的命令前加上 <code>nohup</code> 即可，标准输出和标准错误默认会被重定向到当前目录下的 nohup.out 文件中。此时，终端将不再打印输入输出，但终端依然被占用着。退出终端时，程序将继续运行。</p><p>一般我们可以在结尾加上 <code>&amp;</code> 将命令放入后台运行，终端将不再被占用。</p><h3 id="后台进程管理"><a href="#后台进程管理" class="headerlink" title="后台进程管理"></a>后台进程管理</h3><h4 id="jobs-管理作业"><a href="#jobs-管理作业" class="headerlink" title="jobs 管理作业"></a>jobs 管理作业</h4><p>通过 <code>jobs</code> 命令可以看到<strong>由当前终端创建</strong>的后台作业的运行状况。编号为作业号（jobspec）。</p><ul><li><p><code>bg %JOBSPEC</code> 可以让后台 Stopped 的命令继续 Running</p></li><li><p><code>fg %JOBSPEC</code> 可以让后台的作业来前台执行</p></li><li><p><code>Ctrl+Z</code> 可以把前台执行的作业送进后台并挂起</p></li></ul><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">jobs</span></span><br><span class="line">[1]-  Running                 <span class="built_in">nohup</span> ping www.ibm.com &amp;  (wd: ~/mirai-api-http/mcl-installer)</span><br><span class="line">[2]+  Running                 <span class="built_in">nohup</span> ping www.baidu.com &amp;</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">fg</span> %1</span><br><span class="line"><span class="built_in">nohup</span> ping www.ibm.com  (wd: ~/mirai-api-http/mcl-installer)</span><br><span class="line">^Cazure_root@UbuntuVM:~$ <span class="built_in">jobs</span></span><br><span class="line">[2]+  Running                 <span class="built_in">nohup</span> ping www.baidu.com &amp;</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">fg</span> %2</span><br><span class="line"><span class="built_in">nohup</span> ping www.baidu.com</span><br><span class="line">^Z</span><br><span class="line">[2]+  Stopped                 <span class="built_in">nohup</span> ping www.baidu.com</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">bg</span> %2</span><br><span class="line">[2]+ <span class="built_in">nohup</span> ping www.baidu.com &amp;</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">jobs</span></span><br><span class="line">[2]+  Running                 <span class="built_in">nohup</span> ping www.baidu.com &amp;</span><br></pre></td></tr></tbody></table></figure><p><code>jobs</code> 的缺点在于，一旦丢失 ssh 连接，将再也无法管理作业。</p><h4 id="ps-搜索进程"><a href="#ps-搜索进程" class="headerlink" title="ps 搜索进程"></a>ps 搜索进程</h4><p>Process Status 类似于 Windows 的任务管理器，可以显示系统的所有进程。编号为 PID。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">nohup</span> ping www.ibm.com &amp;</span><br><span class="line">[1] 1408536</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">nohup</span>: ignoring input and appending output to <span class="string">'nohup.out'</span></span><br><span class="line">^C</span><br><span class="line">azure_root@UbuntuVM:~$ ps -ef | grep 1408536</span><br><span class="line">azure_r+ 1408536 1407733  0 19:01 pts/0    00:00:00 ping www.ibm.com</span><br><span class="line">azure_r+ 1408544 1407733  0 19:01 pts/0    00:00:00 grep --color=auto 1408536</span><br></pre></td></tr></tbody></table></figure><p>可以附加 <code>grep -v grep</code> 来忽略 grep 自身进程。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ ps -ux | grep ping | grep -v grep</span><br><span class="line">azure_r+ 1408536  0.0  0.2   8964  2700 pts/0    S    19:01   0:00 ping www.ibm.com</span><br></pre></td></tr></tbody></table></figure><p>其中，<code>ps -ef</code> 和 <code>ps -ux</code> 的区别在于输出风格和内容的不同，<code>ps -ux</code> 和 <code>ps -aux</code> 的区别在于进程所属用户不同。对我而言，<code>ps -ux</code> 获得的信息更直接。</p><h4 id="killl-结束进程"><a href="#killl-结束进程" class="headerlink" title="killl 结束进程"></a>killl 结束进程</h4><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -9 PID</span><br></pre></td></tr></tbody></table></figure><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> %JOBSPEC</span><br></pre></td></tr></tbody></table></figure><p><code>-9</code> 是可选的，用于强制杀死进程。若要使用作业号，需要在 jobspec 前加上 <code>%</code>。纯数字会被认为是 PID。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">kill</span> %2</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">jobs</span></span><br><span class="line">[2]+  Terminated              <span class="built_in">nohup</span> ping www.baidu.com</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">kill</span> -9 %2</span><br><span class="line">-bash: <span class="built_in">kill</span>: %2: no such job</span><br><span class="line">azure_root@UbuntuVM:~$ <span class="built_in">jobs</span></span><br><span class="line">azure_root@UbuntuVM:~$</span><br></pre></td></tr></tbody></table></figure><h3 id="更改默认的-nohup-out"><a href="#更改默认的-nohup-out" class="headerlink" title="更改默认的 nohup.out"></a>更改默认的 nohup.out</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">nohup</span> python file.py &gt; log.txt 2&gt;&amp;1 &amp;</span><br></pre></td></tr></tbody></table></figure><p><code>&gt;</code> 表示重定向标准输出（STDOUT），这里重定向到了当前目录下的 log.txt，不存在此文件时会自动创建。</p><p>若无 <code>2&gt;&amp;1</code> 则错误信息不会重定向。</p><h2 id="setsid"><a href="#setsid" class="headerlink" title="setsid"></a>setsid</h2><p>nohup 无疑能通过忽略 HUP 信号来使我们的进程避免中途被中断。除此之外，如果我们的进程不属于接受 HUP 信号的终端的子进程，那么自然也就不会受到 HUP 信号的影响了。setsid 能帮助我们做到这一点。让我们先来看一下 setsid 的帮助信息：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">SETSID(8)                 Linux Programmer’s Manual                 SETSID(8)</span><br><span class="line">NAME</span><br><span class="line">       setsid - run a program <span class="keyword">in</span> a new session</span><br><span class="line">SYNOPSIS</span><br><span class="line">       setsid program [ arg ... ]</span><br><span class="line">DESCRIPTION</span><br><span class="line">       setsid runs a program <span class="keyword">in</span> a new session.</span><br></pre></td></tr></tbody></table></figure><p>可见 setsid 的使用也是非常方便的，也是只需要在要处理的命令前加上 <code>setsid</code> 即可，不过 shell 依然会被占用，<code>Ctrl+C</code> 和 <code>Ctrl+Z</code> 也不会生效。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ setsid ping www.google.com</span><br><span class="line">azure_root@UbuntuVM:~$ ps -ef | grep www.google.com | grep -v grep</span><br><span class="line">azure_r+ 1409166       1  0 19:43 ?        00:00:00 ping www.google.com</span><br></pre></td></tr></tbody></table></figure><p>值得注意的是，此例中我们的 PID 为 31094，而它的 PPID（父 ID）为 1（即 init 进程 ID），并不是当前终端的进程 ID。这一点与 nohup 例不同。</p><h3 id=""><a href="#" class="headerlink" title="&amp;"></a>&amp;</h3><p>这里还有一个关于 subshell 的小技巧。我们知道，将一个或多个命名包含在 <code>()</code> 中就能让这些命令在子 shell 中运行，从而扩展出很多有趣的功能，我们现在讨论的就是其中之一。</p><p>当我们将 <code>&amp;</code> 也放入 <code>()</code> 内之后，我们就会发现所提交的作业并不在作业列表中，也就是说，是无法通过 jobs 来查看的，<code>Ctrl+C</code> 和 <code>Ctrl+Z</code> 同样不会生效。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">azure_root@UbuntuVM:~$ (ping www.office.com &amp;)</span><br><span class="line">azure_root@UbuntuVM:~$ ps -ef | grep www.office.com | grep -v grep</span><br><span class="line">azure_r+ 1409270       1  0 19:56 pts/0    00:00:00 ping www.office.com</span><br></pre></td></tr></tbody></table></figure><h2 id="disown"><a href="#disown" class="headerlink" title="disown"></a>disown</h2><h3 id="场景-1"><a href="#场景-1" class="headerlink" title="场景"></a>场景</h3><p>我们已经知道，如果事先在命令前加上 nohup 或者 setsid 就可以避免 HUP 信号的影响。但是如果我们未加任何处理就已经提交了命令，该如何补救才能让它避免 HUP 信号的影响呢？</p><h3 id="解决方法-1"><a href="#解决方法-1" class="headerlink" title="解决方法"></a>解决方法</h3><p>此时再想用 nohup 或者 setsid 已经为时已晚，只能用作业调度和 <code>disown</code> 来解决了。让我们来看一下 disown 的帮助信息：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">disown</span> [-ar] [-h] [jobspec ...]</span><br><span class="line">Without options, each jobspec is  removed  from  the  table  of</span><br><span class="line">active  <span class="built_in">jobs</span>.   If  the -h option is given, each jobspec is not</span><br><span class="line">removed from the table, but is marked so  that  SIGHUP  is  not</span><br><span class="line">sent  to the job <span class="keyword">if</span> the shell receives a SIGHUP.  If no jobspec</span><br><span class="line">is present, and neither the -a nor the -r option  is  supplied,</span><br><span class="line">the  current  job  is  used.  If no jobspec is supplied, the -a</span><br><span class="line">option means to remove or mark all <span class="built_in">jobs</span>; the -r option  without</span><br><span class="line">a  jobspec  argument  restricts operation to running <span class="built_in">jobs</span>.  The</span><br><span class="line"><span class="built_in">return</span> value is 0 unless a jobspec does  not  specify  a  valid</span><br><span class="line">job.</span><br></pre></td></tr></tbody></table></figure><ul><li>用 <code>disown -h JOBSPEC</code> 来使某个作业忽略 HUP 信号。</li><li>用 <code>disown -ah</code> 来使所有的作业都忽略 HUP 信号。</li><li>用 <code>disown -rh</code> 来使正在运行的作业忽略 HUP 信号。</li></ul><p>需要注意的是，当使用过 disown 之后，会将把目标作业从作业列表中移除，我们将不能再使用 jobs 来查看它，但 ps 依然可以查找到它。</p><p>这种方法的操作对象是作业，如果需要通过 jobs 命令得到作业列表，在运行命令时应该在结尾添加 <code>&amp;</code> 来使命令成为一个作业并在后台运行。</p><p>如果没有把命令作为作业来运行，我们需要按下 <code>Ctrl+Z</code> 将前台进程送入后台并挂起，然后才可以用 jobs 命令查询它的作业号，接着用 <code>bg %JOBSPEC</code> 让它继续运行。需要注意的是，如果挂起会影响当前进程的运行结果，请慎用此方法。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;用 ssh 登录远程的 Linux 服务器，某些程序或脚本会在前台持续运行、占用 shell 窗口，且终端连接断开时任务也会中止。如何让运行命令提交后不受本地关闭终端窗口或网络断开连接的干扰呢？本文列举了一些实用的命令。&lt;/p&gt;
&lt;/blockqu</summary>
      
    
    
    
    <category term="运维" scheme="https://www.pil0txia.com/categories/%E8%BF%90%E7%BB%B4/"/>
    
    <category term="系统运维" scheme="https://www.pil0txia.com/categories/%E8%BF%90%E7%BB%B4/%E7%B3%BB%E7%BB%9F%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="系统运维" scheme="https://www.pil0txia.com/tags/%E7%B3%BB%E7%BB%9F%E8%BF%90%E7%BB%B4/"/>
    
    <category term="Linux" scheme="https://www.pil0txia.com/tags/Linux/"/>
    
    <category term="操作系统" scheme="https://www.pil0txia.com/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="后台" scheme="https://www.pil0txia.com/tags/%E5%90%8E%E5%8F%B0/"/>
    
    <category term="挂起" scheme="https://www.pil0txia.com/tags/%E6%8C%82%E8%B5%B7/"/>
    
    <category term="进程管理" scheme="https://www.pil0txia.com/tags/%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86/"/>
    
    <category term="作业调度" scheme="https://www.pil0txia.com/tags/%E4%BD%9C%E4%B8%9A%E8%B0%83%E5%BA%A6/"/>
    
  </entry>
  
</feed>
