<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Code Life</title>
    <link>https://www.boris1993.com/</link>
    <description>boris1993的个人博客</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, Boris Zhao</copyright>
    <lastBuildDate>Thu, 02 Apr 2026 14:10:59 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <atom:link href="https://www.boris1993.com/rss2.xml" rel="self" type="application/rss+xml"/>
    <atom:link href="https://pubsubhubbub.appspot.com/" rel="hub"/>
    <item>
      <title>实操基金转托管 —— 从天弘转到招商银行</title>
      <link>https://www.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb.html</link>
      <description>
        <![CDATA[<p>之前定投基金的时候，为了省每次买入的手续费，选择了天弘的自营基金平台。但后来发现，在基金自营平台里购买的份额是不会计入银行统计的总资产的。而且前两天算了下，每天50块定投，每次手续费0.1%，那么全年的手续费也才12块4。我寻思犯不着为了省这点钱把自己的资产搞这么分散，决定把自营平台的份额全部转回招行，以后定投也继续在招行里面进行。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/%E5%9F%BA%E9%87%91/">基金</category>
      <category domain="https://www.boris1993.com/tags/%E8%BD%AC%E6%89%98%E7%AE%A1/">转托管</category>
      <pubDate>Fri, 13 Feb 2026 23:20:48 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>之前定投基金的时候，为了省每次买入的手续费，选择了天弘的自营基金平台。但后来发现，在基金自营平台里购买的份额是不会计入银行统计的总资产的。而且前两天算了下，每天50块定投，每次手续费0.1%，那么全年的手续费也才12块4。我寻思犯不着为了省这点钱把自己的资产搞这么分散，决定把自营平台的份额全部转回招行，以后定投也继续在招行里面进行。</p><span id="more"></span><p style="text-align: center; color: red;">    <b>以下内容仅为我自己操作转出的记录，仅供参考</b>    <br/>    <b>一切请以银行和基金平台提供的信息为准</b></p><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>在转出之前，先确保在直销平台中所有的交易都已经确认，并终止定投计划。并且要和银行确认好，托管转入的份额会不会统计在银行的总资产中。</p><p>天弘基金平台支持在网页上自助完成一步转托管，在提交之前，需要先知道招商银行的席位号、网点号（这个似乎只有天弘基金转出时才需要）、转入的基金账号。</p><p>我从基金平台和银行客服问到的招商银行席位号和网点号都是<code>007</code>，基金账号可以从招商银行的手机银行里面查到。</p><ul><li>登录招商银行手机银行，前往<code>财富</code>页面，再点<code>基金</code>，然后点击右上角小人图标</li></ul><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/look-for-fund-account-step-1.jpeg"></p><ul><li>在弹出的菜单里点<code>我的基金账号</code></li></ul><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/look-for-fund-account-step-2.jpeg"></p><ul><li>然后在列表里找<code>天弘基金</code>，点进去</li></ul><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/my-fund-account-in-bank.jpeg"></p><ul><li>在<code>我的基金账号</code>页面里找到<code>关联基金交易账号</code>，这就是要转入的基金账号</li></ul><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/fund-account-details.jpeg"></p><p>如果在基金账号里找不到天弘基金，那就先在银行里按照最低金额买一次，等基金公司确认之后再找。在银行里找不到基金公司多半是因为之前没在银行里开过这家公司的账户，买一份代销等确认好了之后，账户就开好了。</p><h2 id="提交转出申请"><a href="#提交转出申请" class="headerlink" title="提交转出申请"></a>提交转出申请</h2><p>转出操作需要到天弘基金网页版操作。登录到自己的账户，在列表里找到要转出的直销份额，点击<code>其他</code>菜单中的<code>转托管出</code>。</p><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/submit-transfer-out-step-1.jpeg"></p><p>然后分别填写要转出的份额、招商银行的席位号（对方销售商）、网点号（对方份额托管网点），和刚刚查到的基金账号（对方交易账号）。</p><p><img data-src="https://blog-static.boris1993.com/transfer-of-custody-out-from-tianhong-to-cmb/submit-transfer-out-step-2.jpeg"></p><p>确认无误后，点击下一步，并在弹出的窗口中再次确认，就提交好了。然后就是等基金平台确认托管转出交易。如果确认失败，可以到基金平台找人工客服凭姓名和身份证号查询失败原因。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>新玩具之文石Leaf3C电纸书阅读器</title>
      <link>https://www.boris1993.com/new-toy-e-ink-reader-boox-leaf3c.html</link>
      <description>
        <![CDATA[<p>上周又给自己买了个玩具 —— 文石Leaf 3C电纸书阅读器。用了一周，感觉挺有意思，随手写一篇文章聊聊使用体验，顺便分享一些技巧。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/%E7%94%B5%E7%BA%B8%E4%B9%A6/">电纸书</category>
      <category domain="https://www.boris1993.com/tags/%E9%98%85%E8%AF%BB%E5%99%A8/">阅读器</category>
      <category domain="https://www.boris1993.com/tags/%E6%96%87%E7%9F%B3/">文石</category>
      <pubDate>Fri, 15 Aug 2025 17:43:31 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>上周又给自己买了个玩具 —— 文石Leaf 3C电纸书阅读器。用了一周，感觉挺有意思，随手写一篇文章聊聊使用体验，顺便分享一些技巧。</p><span id="more"></span><h2 id="为什么要买它"><a href="#为什么要买它" class="headerlink" title="为什么要买它"></a>为什么要买它</h2><p>决定买电纸书阅读器主要有两个原因。之前我一直用iPad Pro看书，但这玩意又大又重，用起来不方便，拿在手里不舒服不说，通勤路上还得反复从包里掏出来再塞回去，实在是麻烦。实体书也买了不少，但是搬家后又懒得翻出来，可怜的书们就只是在箱子里吃灰。所以我总是想找一个小巧的可以随时看书的玩意。正好在同事的桌子上看到一个小巧的阅读器，就决定也搞一个。</p><p>选择文石Leaf 3C也没啥特别的理由，就是这位同事用的Leaf 2，我拿起来大概试了下，尺寸和重量都挑不出啥毛病，加上不想花太多钱，于是就买了这个二手的Leaf 3C。</p><h2 id="初体验"><a href="#初体验" class="headerlink" title="初体验"></a>初体验</h2><p>一拿到手，第一感觉就是，好轻！我终于不用再端着iPad看书了(T_T)！机身只有6毫米厚，重量也只有190克，粗略感受下来，比手上的iPhone 12 Pro大概要轻一半，甚至跟它的磁吸保护壳差不多重。轻薄纤细的机身拿起来就非常的顺手。</p><p>前面板是一整块有点磨砂质感的玻璃，触感细腻，摩擦力也正正好，而且不会留下指纹。占据大部分面积的一块7英寸的Kaleido 3彩墨触屏，右边边缘有两个可以用来翻页的按键，正好适合我用右手拿着看书。如果你不喜欢用右手操作的话，系统层面也支持将整个屏幕旋转90度、180度、270度，来让你以一个你喜欢的姿势阅读。不过这个按键的手感并不是非常好。首先它会晃，其次，它并不是像手机的音量键那样是一个平整的按键，而是按键的中部下面压着一个贴片按钮开关，如果你按的位置很靠近边缘的话，就很有可能根本按不下去。</p><p><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/reader.jpg"></p><p>阅读黑白文本的体验不用说。7英寸，300PPI的屏幕，我觉得完全够用。屏幕还自带前光，可以调节亮度和色温，就算睡前想要看会书也是没问题的，虽然可能对眼睛不太好……</p><p><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/book_display.jpg"></p><p>但要是你想拿它看彩色的漫画，那你可能就要失望了。这块屏幕只能展现4096种颜色（根据维基百科，相当于上世纪八九十年代的Amiga电脑），彩色表现非常的素，没办法展现鲜艳的色彩。实际的显示效果可以看下面的对比图。系统虽然自带了“均衡”和“鲜艳”两种色彩预设，也可以自定义，但恕我眼拙，没看出有什么明显的区别。</p><table><thead><tr><th>显示</th><th>截图</th></tr></thead><tbody><tr><td><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/ccs_display.jpg"></td><td><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/ccs_screenshot.png"></td></tr></tbody></table><p>它的内置阅读器也有个小bug。我目前的系统版本是4.0.2，在使用V3引擎阅读竖版书籍时，书中的插图无法正常显示。比如下面的对比图中，“一九二九年的大崩盘”和“印钞票”这两节之间是有一个插图的，但是以竖排显示时，阅读器无法正常显示插图。好在这个问题可以通过在阅读器内单独讲这本书转为横排来解决。我也向文石开发团队反馈了这个问题，希望将来会修复。</p><table><thead><tr><th>竖排</th><th>横排</th></tr></thead><tbody><tr><td><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/render_vertical_cant_show_inline_picture.png"></td><td><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/render_horizontal_can_show_inline_picture.png"></td></tr></tbody></table><p>还有个小问题是，它会有点阴阳屏。如果仔细盯着它看，就能隐约看到远离按键的一侧要比靠近按键的一侧要稍微暗一点。但查过资料发现，这应该是电纸书的通病，因为前光是从侧面照明的，无法避免亮度不均匀的问题，只能自己慢慢习惯。</p><p>软件上，它的操作系统是基于Android 11深度定制的，除了自带的阅读软件，你还可以安装其他的读书平台（甚至可以直接把Z-Library的客户端装上，就省的用电脑往里面拷书了）。考虑到电纸书的使用场景，系统提供了一个方便的入口来控制应用冻结，并且新安装的软件也可以自动冻结，这样就可以有效避免各种后台进程在你并不需要的时候运行，让续航更稳。实际用下来，续航不用担心，除非你有无可救药的续航焦虑。我每天开着前光，平均用它内置的阅读软件看两小时，一周下来大约会消耗70%电量。不过这是用<strong>内置阅读软件</strong>的情况，用第三方软件的话，耗电就明显会变多。</p><h2 id="买书"><a href="#买书" class="headerlink" title="买书"></a>买书</h2><p>虽然Z-Library上面可以免费下载海量的书籍，但我更愿意花钱购买正版来支持作者。另外我希望买完书之后能自己保存一份电子书的副本，并用任意我喜欢的工具阅读，所以像微信读书和京东书城等国内常用的电子书平台全部被我排除在外 —— 它们只让你在自家的app里阅读，并不能下载，太不自由了。</p><p>而且比起订阅读书软件的会员，我也更喜欢买断制。因为我觉得，只有我买断这本书并且下载到这本书的副本，我才是真正拥有了这本书。而开会员更像是租了某个图书馆的入场资格，虽然整个图书馆的书我可以随便看，但我并不拥有任何一本，平台也可以随时换源甚至下架某本书，而我对此无能为力。更别提有的书只在A平台能看，而另一本书只能在B平台看的这种麻烦事。</p><p>最后我折腾出来一套有点拧巴的买书流程：</p><ul><li>首先去<span class="exturl" data-url="aHR0cHM6Ly93d3cua29iby5jb20vaGsvemg=">Rakuten Kobo<i class="fa fa-external-link-alt"></i></span>、<span class="exturl" data-url="aHR0cHM6Ly9lYm9vay5oeXJlYWQuY29tLnR3Lw==">HyRead<i class="fa fa-external-link-alt"></i></span>、<span class="exturl" data-url="aHR0cHM6Ly9yZWFkbW9vLmNvbS8=">读墨<i class="fa fa-external-link-alt"></i></span>等港台的电子书商城购买，然后通过Calibre的插件把DRM去掉，然后导入阅读器，用它自带的软件阅读。美国亚马逊的Kindle商城里面也可以买，但我还没试过去除Kindle格式电子书的DRM，也不确定阅读器是否支持Kindle的AZW格式。</li><li>如果在上面这三个书店都找不到，那就去微信阅读里面买断这本书，然后再去Z-Library找高质量的epub。我不清楚这样下载到的书在严格意义上还算不算盗版，但至少我为它付费了，读起来也心安理得。</li><li>如果Z-Library上都找不到高质量的副本，那就用第三方工具从微信读书中导出，再将下载到的HTML和资源文件放进Calibre，自己手动做成epub。这做起来就比较耗时耗力了，好在下载器是开源的，我可以修改它的代码为我完成最麻烦的部分。</li></ul><p>目前我的书都是在Rakuten Kobo上面买的，唯一的门槛就是，你要有一张VISA或者万事达卡组织的借记卡或信用卡，或者有一个PayPal账户。但是各家银行都有发行VISA和万事达的信用卡，中国区PayPal注册也很简单，所以也不算什么大问题。哦对，如果你买的书很贵的话，记得去网上搜Kobo的折扣码，我用过一个7折和一个8折的折扣，还是很爽的。便宜书就别用了，因为一个折扣码只能用一次，别浪费在便宜的书上。</p><p>还有一点要注意，因为这些平台都位于香港或台湾，所以大部分书都是繁体中文。如果你平时不习惯看繁体字，那买之前最好先确认一下。</p><h2 id="使用Calibre去除DRM"><a href="#使用Calibre去除DRM" class="headerlink" title="使用Calibre去除DRM"></a>使用Calibre去除DRM</h2><p style="text-align: center;">    <b>⚠️ ⚠️ ⚠️ 本节内容所述教程仅应用来方便自己阅读 ⚠️ ⚠️ ⚠️</b><br/>    <b>⚠️ ⚠️ ⚠️ 尊重知识产权 ⚠️ ⚠️ ⚠️</b><br/>    <b>⚠️ ⚠️ ⚠️ 不要制作和分发盗版书 ⚠️ ⚠️ ⚠️</b></p><p>在Kobo购买的电子书都是有DRM保护的。虽然我们可以使用Adobe Digital Editions下载得到epub文件，但是这些文件都受Adobe DRM保护，拷进阅读器也无法打开。为了能脱离Kobo平台，我们就需要使用Calibre和DeDRM插件来去除DRM。</p><p>首先到<span class="exturl" data-url="aHR0cHM6Ly9jYWxpYnJlLWVib29rLmNvbS8=">Calibre官网<i class="fa fa-external-link-alt"></i></span>下载并安装Calibre。然后前往<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL25vRFJNL0RlRFJNX3Rvb2xzL3JlbGVhc2VzL2xhdGVzdA==">DeDRM_tools - GitHub Releases<i class="fa fa-external-link-alt"></i></span>下载最新的DeDRM插件。将下载的zip文件解压缩，可以得到两个压缩包<code>DeDRM_plugin.zip</code>和<code>Obok_plugin.zip</code>。</p><p>接下来打开Calibre，点击顶部工具栏的<code>首选项</code>按钮。</p><p>首先进入<code>导入/导出</code>中的<code>保存书籍到硬盘</code>，取消勾选<code>将非英语字符转换为对应英语字符</code>，并点击<code>应用</code>，这样在导出时就可以保留书籍原本的文件名。</p><p>然后在<code>首选项</code>窗口，找到<code>高级选项</code>中的<code>插件</code>并点击，接下来点击<code>从文件加载插件</code>，分别加载<code>DeDRM_plugin.zip</code>和<code>Obok_plugin.zip</code>。安装完成后勾选插件列表上面的<code>仅显示用户自己安装的插件</code>并搜索<code>drm</code>，如果搜到如下图一样的结果，就说明插件安装成功了。</p><p><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/calibre_dedrm_plugin_install_successful.png"></p><p>接下来前往<span class="exturl" data-url="aHR0cHM6Ly93d3cuYWRvYmUuY29tL2dyX2VuL3NvbHV0aW9ucy9lYm9vay9kaWdpdGFsLWVkaXRpb25zL2Rvd25sb2FkLmh0bWw=">Adobe Digital Editions下载页<i class="fa fa-external-link-alt"></i></span>，下载Adobe Digital Editions (ADE)并安装。安装完成后打开ADE，点击<code>帮助</code>菜单中的<code>授权计算机</code>。如果你有Adobe账号的话，就在这里输入账号的邮箱和密码来登录；如果没有的话，就勾选窗口下部的<code>我想要在不使用ID的情况下对我的计算机授权</code>。然后点击<code>授权</code>按钮，等待授权完成。</p><p>接下来，前往Kobo书城的<code>我的账户 -&gt; 书籍</code>，找到你已经购买的电子书，点击书名上方的三个点展开菜单，再点击菜单中的<code>下载</code>。这时候你会下载到一个名为<code>URLLink.acsm</code>的文件。把这个文件拖进ADE，然后ADE会下载这本书的EPUB文件，当然，这个文件是加密的。下载成功后，在ADE中右击刚刚下载的书，并点击<code>在资源管理器中显示文件</code>。</p><p>然后，从刚刚打开的资源管理器窗口中，将你的书拖到Calibre的主界面中，这时候Calibre就会自动解除这本书的DRM并导入它的书库。然后在Calibre中右击这本书，点击菜单<code>打开书籍文件夹 -&gt; 打开书籍文件夹</code>，就能找到去掉了DRM的EPUB文件。然后你就可以把这个文件复制到阅读器中，使用它内置的阅读器阅读了。</p><p><img data-src="https://blog-static.boris1993.com/new-toy-e-ink-reader-boox-leaf3c/calibre_locate_book.png"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>配置RouterOS从内网访问光猫</title>
      <link>https://www.boris1993.com/access-to-fiber-optic-modem-through-lan.html</link>
      <description>
        <![CDATA[<p>我的宽带是自己换了光猫改成了桥接的，平时用起来没问题，只是我很不爽不能直接通过内网访问到光猫的配置页面。倒不是要经常改光猫配置，只是又犯了折腾病，想要把这一块缺失的拼图补上。一开始以为是配一条静态路由的事，但是发现并不奏效。上网学习了别人的成功经验之后，发现我的方向彻底错了，好在正确的操作也并不复杂。</p>
<p>因为我用的是搭载RouterOS的Mikrotik路由器，所以文中涉及的命令都将是RouterOS的语法。不过我也会讲明操作的原理，所以你也可以跟着思路针对你的路由器进行配置。唯一需要注意的就是，要确保你的路由器可以自己配置<code>iptables</code>规则。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/%E5%85%89%E7%8C%AB/">光猫</category>
      <category domain="https://www.boris1993.com/tags/%E6%A1%A5%E6%8E%A5/">桥接</category>
      <category domain="https://www.boris1993.com/tags/RouterOS/">RouterOS</category>
      <pubDate>Thu, 24 Jul 2025 06:51:16 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>我的宽带是自己换了光猫改成了桥接的，平时用起来没问题，只是我很不爽不能直接通过内网访问到光猫的配置页面。倒不是要经常改光猫配置，只是又犯了折腾病，想要把这一块缺失的拼图补上。一开始以为是配一条静态路由的事，但是发现并不奏效。上网学习了别人的成功经验之后，发现我的方向彻底错了，好在正确的操作也并不复杂。</p><p>因为我用的是搭载RouterOS的Mikrotik路由器，所以文中涉及的命令都将是RouterOS的语法。不过我也会讲明操作的原理，所以你也可以跟着思路针对你的路由器进行配置。唯一需要注意的就是，要确保你的路由器可以自己配置<code>iptables</code>规则。</p><span id="more"></span><p>如下是我的网络拓扑，其中：</p><ul><li>光猫的管理地址为<code>192.168.0.1</code>，独占<code>192.168.0.0/24</code>网段</li><li>路由器的地址为<code>192.168.1.1</code>，它和内网的各个设备占用<code>192.168.1.0/24</code>网段</li></ul><p><img data-src="https://blog-static.boris1993.com/access-to-fiber-optic-modem-through-lan/network-topology.png"></p><p>那么要通过路由器访问到光猫的话，只需要做下面三件事：</p><ul><li>给路由器的WAN口分配一个<code>192.168.0.0/24</code>网段的地址，比如<code>192.168.0.2</code></li></ul><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/ip/address/<span class="built_in">add</span> <span class="attribute">address</span>=192.168.0.2/24 <span class="attribute">comment</span>=<span class="string">&quot;Modem access&quot;</span> <span class="attribute">interface</span>=ether1 <span class="attribute">network</span>=192.168.0.0</span><br></pre></td></tr></table></figure><ul><li>在防火墙的<code>nat</code>表中配置如下规则</li></ul><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/ip/firewall/nat/<span class="built_in">add</span> <span class="attribute">action</span>=masquerade <span class="attribute">chain</span>=srcnat <span class="attribute">comment</span>=<span class="string">&quot;Access to modem subnet&quot;</span> <span class="attribute">dst-address</span>=192.168.0.0/24 <span class="attribute">src-address</span>=192.168.1.0/24</span><br></pre></td></tr></table></figure><p>这条规则可以让路由器在收到向光猫发出的数据包后进行一次源地址伪装，使其看起来像是从路由器发出的，并在路由器收到响应的数据包后，再将其目标地址和端口修改会发出请求的电脑。</p><ul><li>在防火墙的<code>mangle</code>表中配置如下规则</li></ul><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/ip/firewall<span class="built_in">/mangle </span><span class="built_in">add</span> <span class="attribute">action</span>=accept <span class="attribute">chain</span>=prerouting <span class="attribute">comment</span>=<span class="string">&quot;Access to modem subnet&quot;</span> <span class="attribute">dst-address</span>=192.168.0.0/24 <span class="attribute">src-address</span>=192.168.1.0/24</span><br></pre></td></tr></table></figure><p>这条规则将告知路由器在收到从内网访问光猫的数据包时，直接放行这个数据包，不再进行任何其他的标记或改变路由行为的操作。这条规则并不是必须的，但是如果你在<code>mangle</code>表中有其他的规则，那还是加上它比较好，以避免访问到光猫的数据包被错误的标记，或者产生非预期的行为。</p><p>上面两条RouterOS命令我也用ChatGPT翻译成了<code>iptables</code>命令，供参考：</p><figure class="highlight shell"><table><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="meta prompt_"># </span><span class="language-bash">/ip firewall nat add action=masquerade chain=srcnat comment=<span class="string">&quot;Access to modem subnet&quot;</span> dst-address=192.168.0.0/24 src-address=192.168.1.0/24</span></span><br><span class="line">iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 192.168.0.0/24 -j MASQUERADE -m comment --comment &quot;Access to modem subnet&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">/ip firewall/mangle add action=accept chain=prerouting comment=<span class="string">&quot;Access to modem subnet&quot;</span> dst-address=192.168.0.0/24 src-address=192.168.1.0/24</span></span><br><span class="line">iptables -t mangle -A PREROUTING -s 192.168.1.0/24 -d 192.168.0.0/24 -j ACCEPT -m comment --comment &quot;Access to modem subnet&quot;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>我的信用卡优化记录 - 2025年</title>
      <link>https://www.boris1993.com/credit-card-clean-up-2025.html</link>
      <description>
        <![CDATA[<p>上个月翻小红书的时候，被推送了些信用卡权益的帖子，自己想想我从2016年毕业办下第一张招行信用卡至今，好像啥权益也没捞到，净往外花钱了。再一研究，发现什么返现啊贵宾厅啊之类的权益，也并不需要花很大精力或者遥不可及。那么，开始优化！</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/recoding-my-life/">生活记录</category>
      <category domain="https://www.boris1993.com/tags/%E4%BF%A1%E7%94%A8%E5%8D%A1/">信用卡</category>
      <category domain="https://www.boris1993.com/tags/%E8%96%85%E7%BE%8A%E6%AF%9B/">薅羊毛</category>
      <pubDate>Fri, 20 Jun 2025 22:53:03 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>上个月翻小红书的时候，被推送了些信用卡权益的帖子，自己想想我从2016年毕业办下第一张招行信用卡至今，好像啥权益也没捞到，净往外花钱了。再一研究，发现什么返现啊贵宾厅啊之类的权益，也并不需要花很大精力或者遥不可及。那么，开始优化！</p><span id="more"></span><h2 id="注销的卡"><a href="#注销的卡" class="headerlink" title="注销的卡"></a>注销的卡</h2><p>开始之前先盘一下库存和年费：</p><ul><li>招行银联金卡 - 6笔消费刷免年费</li><li>招行全币种VISA卡 - 有效期内免年费</li><li>招行全币种JCB卡 - 有效期内免年费</li><li>招行运通金卡 - 6笔消费刷免年费</li><li>招行英雄联盟联名银联金卡 - 6笔消费刷免年费</li><li>招行和卡银联金卡 - 6笔消费刷免年费</li><li>交行魔都优逸白金银联卡 - 6笔消费刷免年费</li><li>浦发魔都白金银联卡 - 终身免年费</li></ul><p>看得出来，光是要把年费刷掉，就要费一点精力，而且这些卡几乎没有任何权益，所以非必要就注销。</p><ul><li>招行：<ul><li>只留下银联金卡、VISA和JCB。银联用来备用，而且买苹果设备还能享受24期免息分期。JCB绑了日区Apple ID做iCloud付款卡，而现在日区又不能新绑非日本发行的JCB卡，所以这张万万不能没。VISA反正免年费，留着以防万一。</li><li>运通在当时申请的时候还是有效期内免年费的，但是现在运通卡支持人民币结算后，就要收年费了。所以注销。该说不说，最早拿到的是运通Clear卡，卡面很好看（我也是冲着卡面才办的），但后面换发就变成了百夫长经典金卡。</li><li>英雄联盟卡是还没毕业那会就申请的校园信用卡，为的是打英雄联盟对局就能得银行积分，但是就算最上头的那段时间似乎也没攒下多少积分，而且那会似乎都已经是活动的尾声了。现在留着也没用，所以注销。</li><li>和卡……嘛，它其实是顶替了崩坏三联名八重樱银联卡，但到期的时候已经没有联名卡板了，给我换成了和卡。本来就是收藏来用的，所以注销（想来当初就不应该激活的）。</li></ul></li><li>交行：这卡好像是刚工作那会，银行业务员直接来公司给我们推销的。想想，刚毕业，也没啥钱，就能下个白金卡，自然是忍不住诱惑就办了。结果最后沦为逢年过节回老家的时候刷地铁用。没权益还要惦记着刷免年费，所以注销。</li><li>浦发：好像也是刚工作的时候银行业务员直接来公司推销的，当时一听，白金卡，还终身免年费，那干脆办下来，放着不用也没啥损失。结果好像去年的时候，这张卡因为太久不用，在银行系统里变成了一个很奇怪的状态，就是卡虽然没注销，但是在浦大喜奔里面又没法查到状态，点进去就会报错。既然如此，干脆销了一了百了。</li></ul><h2 id="新办的卡"><a href="#新办的卡" class="headerlink" title="新办的卡"></a>新办的卡</h2><p>说起权益，我主要关心消费返现和机场贵宾厅。今年4月份去香港旅游回来的那天，我累到坐在登机口的长椅上几乎晕厥，那时候就想，如果能给我个沙发让我舒舒服服小眯一会就好了。那机场的贵宾厅岂不是就能满足？但是毕竟每年我也就是往返老家以及出去旅游一两次的时候才会坐飞机，满打满算一年也就坐4到8趟，而刚性年费的高端白金卡提供的几十次甚至无限次贵宾厅，对我来说绝对是溢出的。</p><p>所以最后需求就固定成，有消费返现，有机场贵宾厅，权益达成条件不要太复杂，可以免年费，而且最好能集中在同一家银行，这样可以更集中地累计积分。</p><p>经过研究之后，我最终得到了这样的一个方案（注意，以下关于权益的描述仅代表我个人的理解，请以银行app中的实际描述为准）：</p><ul><li>广发多利白金卡<ul><li>权益：<ul><li>美团和滴滴消费返现10%（还有另外几个渠道也会返10%，但我不常用就不写了）。</li><li>支付宝、微信、云闪付消费返1%。</li><li>每自然月最多积累100元返现。每自然月消费满3000元后，次月就可以到app的权益中心用累计的返利金兑换返利券（目前是攒满100就能兑5张20的，消费入账过几天后会自动抵扣，不用手动操作）。</li></ul></li><li>缺点：这张卡消费没有积分。</li><li>年费：首年免年费，消费12笔或连续12个月绑定第三方支付（微信、支付宝、京东三选一）免次年年费。说白了就是往支付宝上一绑就不用操心了。</li><li>使用场景：上班的时候用美团点外卖，以及偶尔用滴滴打车，返利金攒够100元并且总消费满3000元就换别的卡刷。毕竟消费没有积分，这个月溢出的返利金也不会结转到次月，再继续刷下去又成了有出没进。</li></ul></li><li>广发万事达真情白金卡<ul><li>权益：<ul><li>京东、拼多多、美团等渠道消费8倍积分。</li><li>微信、支付宝消费1.5倍积分。</li><li>境外消费2倍积分。</li></ul></li><li>年费：首年免年费，消费12笔或连续12个月绑定第三方支付（微信、支付宝、京东三选一）免次年年费。既然这个在京东消费有8倍积分，那就挂在京东支付上免年费。</li><li>使用场景：既然美团消费8倍积分，那返利卡刷满之后，再点外卖就刷这个，此外就是在京东买东西也刷这个。</li></ul></li><li>广发美国运通真情白金卡<ul><li>权益：<ul><li>网购消费可以得三倍积分，亲测下馆子吃饭用支付宝付款，或者用外卖平台点外卖，然后用这张卡付款，就能得三倍积分。</li><li>在运通体系中是蓝盒子SELECT级别，绑定运通微信服务号后还能享受运通卡组织的一些权益，一大堆但都聊胜于无，就不写了。</li></ul></li><li>年费：首年免年费，当年年费周期内消费12笔免次年年费。</li><li>使用场景：返利卡刷满之后就主刷这个，用日常各种消费积累积分。吃吃喝喝啥的咋也能刷出来12笔，顺便就把年费免了。</li></ul></li><li>广发银联鼎极白金卡（臻瑞版）<ul><li>权益：<ul><li>当月消费满3000元后，次月起两个月内可免费享受2次广发冠名机场和高铁站贵宾厅。</li><li>境内消费2倍积分，境外消费3倍积分。</li><li>可以15000积分兑换100航空里程。</li><li>消费积分在5年到期后可以再延续5年有效期，也就是说你累积的积分可以10年不过期。</li><li>附带一张Infinite级别的VISA卡，可以享受VISA卡组织的权益。</li><li>除此之外还有很多别的权益，但我用不上，就不写了。</li></ul></li><li>年费：首年免年费，激活后365天内消费满48笔，或激活日起连续12个月绑定第三方支付（微信、支付宝、京东三选一）免次年年费。说白了也是往支付宝一绑就不用操心了。</li><li>使用场景：这张卡消费权益不如上面几张，但是它的重点是可以用15000积分换100航司里程，我逢年过节往返老家主要坐国航的航班，那么除了坐飞机积累的里程，还可以用这张卡兑换国航里程。所以这其实是一张扔抽屉里面的工具卡，不消费，只用它延长积分有效期以及兑换航司里程，</li></ul></li><li>广发万事达给力外币卡<ul><li>权益：境外消费随机返现，返现直接入账，不占用多利白金卡的返利金池。别的记不清了，这张卡暂时还是下架状态，也没法再去查证。</li><li>缺点：app上没有细说“随机返现”的细则，暂不清楚是笔笔返随机金额，还是有的消费给返有的不给。另外，虽然这张卡是万事达World级别，但是不能参与万事达消费大挑战活动。</li><li>年费：首年免年费，该卡刷卡达3笔，或当前年费周期下本人所有信用卡消费15笔，免次年年费。因为我日常就刷上面那些广发信用卡，所以这张就会连带着免掉年费。</li><li>使用场景：绑Netflix之类的所有能绑的境外平台，薅月费年费的羊毛，以及出境游可以带上。</li></ul></li></ul><p>以上这些就是日常消费的主力卡，平时靠多利白金卡和万事达真情白金卡薅返利金和积分的羊毛，如果有坐飞机出行的需求，就拿鼎极白金卡提前一个多月买好往返机票，这就几乎能凑满贵宾厅的3000元门槛，就算不够也就缺几百块，点点外卖下下馆子，或者给公交卡和Steam充个值就有了。而且都是同一家银行，积分非常集中，并且年费全部都可以免掉。需求达成。</p><p>除了上面这些，我还办了另外几张锦上添花的。</p><ul><li>中国银行全币种国际芯片威士白金卡 - 首先这张卡是免年费的，其次它是VISA Infinite级别，还有开户礼羊毛可以薅。计划薅完开户礼后就放在抽屉里当作备用。</li><li>中国银行JCB招财猫白金卡 - 这张卡也是免年费的，而且是日元入账，免去了日元 -&gt; 美元 -&gt; 人民币的二次汇率折损。同样也有开户礼羊毛可以薅。此外，这张卡是JCB Platinum级别，每年可以免费享受6次日本和夏威夷的贵宾厅，并且可以绑定Lounge Key，每年免费享受2次日本和夏威夷以外的贵宾厅。所以计划是薅完开户礼后就当备用，将来去日本旅游的时候带上，以及将来绑到日区iCloud消费。</li><li>中国银行莫奈睡莲万事达卡 - 这张卡也免年费，是万事达World级别，并且可以参与万事达消费大挑战活动，而且最重要的是卡面很好看。目前还是放在抽屉里备用，将来出境游的时候可以带上试试参与消费大挑战活动。</li><li>光大阳光车主数字卡财富版紫蕴麒麟 - 这张卡我主要看中它每年给5点贵宾休息室点数，以及月均资产达标3000元就能免年费，可以低成本的拉低我每年贵宾厅的需求成本。这张卡暂时还没办，因为我是这个月才往光大银行存了3000元，要在下个月办卡才能免掉年费。</li></ul><p>这几张卡在实际使用体验后，我会再发文分享我的使用心得。</p><p>以上。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用Coze工作流将小红书笔记保存至Notion - 工作流配置</title>
      <link>https://www.boris1993.com/save_rednote_to_notion_with_coze_workflow.html</link>
      <description>
        <![CDATA[<p>半个月前我的小红书号不明不白的被封了。而在被封之后我才发现，“我”这个页面也彻底被屏蔽，我再也看不到我收藏的帖子。痛定思痛，既然保存在小红书上的收藏有再也看不到的可能，那么，我把帖子保存到我自己的Notion不就好了？</p>
<p>说干就干，在一顿网上冲浪之后，我看到有人已经实现了利用字节跳动火山引擎的“<span class="exturl" data-url="aHR0cHM6Ly93d3cuY296ZS5jbi8=">扣子（Coze）<i class="fa fa-external-link-alt"></i></span>”实现了这个功能，但作者并没有放出来具体的实现。我一方面很不喜欢需要私信进群这种操作，另一方面还是自己做一个才最符合自己的需求（拜托，我可是程序员欸），所以自己动手实现了一个工作流，可以完整转存帖子的标题、内容、图片到自己的Notion。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/%E5%B0%8F%E7%BA%A2%E4%B9%A6/">小红书</category>
      <category domain="https://www.boris1993.com/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/">工作流</category>
      <category domain="https://www.boris1993.com/tags/Coze/">Coze</category>
      <category domain="https://www.boris1993.com/tags/%E6%99%BA%E8%83%BD%E4%BD%93/">智能体</category>
      <pubDate>Sun, 08 Jun 2025 05:57:34 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>半个月前我的小红书号不明不白的被封了。而在被封之后我才发现，“我”这个页面也彻底被屏蔽，我再也看不到我收藏的帖子。痛定思痛，既然保存在小红书上的收藏有再也看不到的可能，那么，我把帖子保存到我自己的Notion不就好了？</p><p>说干就干，在一顿网上冲浪之后，我看到有人已经实现了利用字节跳动火山引擎的“<span class="exturl" data-url="aHR0cHM6Ly93d3cuY296ZS5jbi8=">扣子（Coze）<i class="fa fa-external-link-alt"></i></span>”实现了这个功能，但作者并没有放出来具体的实现。我一方面很不喜欢需要私信进群这种操作，另一方面还是自己做一个才最符合自己的需求（拜托，我可是程序员欸），所以自己动手实现了一个工作流，可以完整转存帖子的标题、内容、图片到自己的Notion。</p><span id="more"></span><h2 id="大致的功能设计"><a href="#大致的功能设计" class="headerlink" title="大致的功能设计"></a>大致的功能设计</h2><p>关于这个工作流，我希望它能做到这些：</p><ul><li>自动解析小红书分享链接（类似<code>94 某某某发布了一篇小红书笔记，快来看吧！ 😆 zop6CK2fPRDGwIT 😆 http://xhslink.com/a/5ZOdZhppuYteb，复制本条信息，打开【小红书】App查看精彩内容！</code>）的这种。我需要它能从这个分享链接中获取到对应笔记的真实地址。哦对，这条链接就不用试了，因为这是一条我自己编的。</li><li>自动解析小红书笔记，将里面的标题、内容、图片全部提取出来。</li><li>在成功提取出笔记内容之后，它可以将图片上传到Notion，并将其嵌入到Notion文档中。</li><li>可以通过手机直接分享到工作流，而不是拷出来分享链接，再打开Coze手动运行它。</li></ul><p>我不需要它做的是：</p><ul><li>转存视频。因为视频我可以直接保存到相册，或者手机录屏。而且对我来说，视频的信息密度比文字低，就算保存下来，我也要再把内容提炼成文字版保存。</li><li>转存评论。评论区的内容通常会很杂乱，没有全部转存的必要。有价值的内容我再手动拷到自己的帖子里面就是了。</li><li>监控小红书收藏夹。我觉得它暂时没这个本事，要做到这个，我可能需要单独开发一个程序。</li></ul><h2 id="简单介绍一下它具体是怎么工作的"><a href="#简单介绍一下它具体是怎么工作的" class="headerlink" title="简单介绍一下它具体是怎么工作的"></a>简单介绍一下它具体是怎么工作的</h2><p>这个工作流看起来是这样的</p><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/coze_workflow_save_rednote_to_notion_overview.png"></p><p>具体工作起来，会进行下面这几步操作：</p><ul><li>首先使用配置好的token尝试连接Notion，如果连不上，那也就别费劲干后面的事了，赶紧报错通知用户检查</li><li>接下来先解析分享链接，把实际的短链接拿出来，然后再用短链接换到对应的长链接</li><li>得到长链接后，就可以调用插件得到这个笔记的标题、内容、图片地址，并能以JSON格式输出</li><li>得到了笔记的内容之后，我就可以把图片逐个下载下来，再上传到Notion，然后使用这些内容创建一篇帖子</li><li>最后，如果帖子创建成功，那么返回帖子的链接</li></ul><h2 id="在Notion中要做的准备"><a href="#在Notion中要做的准备" class="headerlink" title="在Notion中要做的准备"></a>在Notion中要做的准备</h2><p>我的这个工作流是基于Notion的数据库实现的，所以首先需要在Notion中创建一个新的数据库。这个数据库将会包含三个列：</p><ul><li>Name - 数据库默认存在并不可删除的列，作为帖子的标题</li><li>Images - 一个<code>Files &amp; media</code>类型的列，图片将在这里保存及展示</li><li>Tags - 一个<code>Multi-select</code>类型的列，用于保存帖子的标签</li></ul><p>最终你应该会得到一个这样的数据库：</p><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/notion_database.png"></p><p>接下来，因为我们需要调用Notion的API来上传图片和创建帖子，所以我们需要前往<code>Notion Integrations</code>页面为其创建一个API key，配置它的权限，并将刚才创建好的数据库与它绑定。</p><p>首先点<code>New Integration</code>创建一个API key。名字无所谓，类型选<code>Internal</code>。</p><p>创建成功后会自动跳转到<code>Configuration</code>页面。在<code>Capabilities</code>的<code>Content Capabilities</code>中勾选<code>Read</code>、<code>Update</code>和<code>Insert</code>权限，然后在<code>User Capabilities</code>中选择<code>No user information</code>。</p><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/notion_integration_capabilities.png"></p><p>接下来到<code>Access</code>页面，点<code>Edit access</code>并勾上刚刚创建的数据库。</p><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/notion_integration_access.png"></p><p>至此Notion部分的准备工作就结束了。</p><h2 id="创建工作流"><a href="#创建工作流" class="headerlink" title="创建工作流"></a>创建工作流</h2><p>接下来就是到扣子平台创建工作流了。登陆后点击<code>创建</code>，然后选择<code>创建智能体</code>，起个名字点击创建，就会进入智能体的编辑界面。</p><h3 id="配置工作流的变量"><a href="#配置工作流的变量" class="headerlink" title="配置工作流的变量"></a>配置工作流的变量</h3><p>首先点击<code>变量</code>部分的加号，并创建三个变量：</p><ul><li><code>rednote_cookie</code> - 小红书的cookie，解析笔记内容要用到，并且这个cookie似乎不能来自被封禁的账号</li><li><code>notion_page_root</code> - Notion数据库的ID，在Notion中点进去上面创建的数据库，将浏览器地址栏中的URL去掉<code>https://www.notion.so/</code>和问号后面的东西，剩下的这部分就是数据库ID</li><li><code>notion_secret</code> - Notion的API key，回到创建API key的页面，点击<code>Internal Integration Secret</code>的<code>Show</code>并把内容拷过来就行</li></ul><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/coze_workflow_variables.png"></p><p>然后，就可以开始开发工作流本体了。</p><h3 id="配置工作流组件"><a href="#配置工作流组件" class="headerlink" title="配置工作流组件"></a>配置工作流组件</h3><p>这里我会逐个介绍每个节点的配置，至于节点之间的连接关系，如果你看文字看不明白的话，就参考上面的截图。</p><ul><li>在<strong>开始</strong>节点添加一个<code>string</code>类型的输入，名为<code>rednote_share</code>，用来传入小红书的分享链接</li><li>添加<a href="https://www.coze.cn/store/plugin/7489686963055018025"><strong>notion数据库助手服务的get_database_info节点</strong></a>，添加两个输入变量<ul><li><code>secret</code>引用用户变量<code>notion_secret</code></li><li><code>url</code>填写数据库的URL，就是从浏览器地址栏复制出来的那个，记得去掉问号和后面的东西</li></ul></li><li>添加一个<strong>选择器</strong>节点，检查<strong>get_database_info</strong>节点的<code>code</code>输出字段是否不等于0。如果满足条件，即说明数据库连接不成功，应当报错并退出</li><li>为上一步的<strong>选择器</strong>节点的<strong>如果</strong>添加一个<strong>输出</strong>节点<ul><li>添加两个输出变量<ul><li><code>database_id</code>引用<strong>get_database_info</strong>的输出字段<code>id</code></li><li><code>message</code>引用<strong>get_database_info</strong>的输出字段<code>msg</code></li></ul></li><li>配置输出内容为<code>连接Notion数据库&#123;&#123;database_id&#125;&#125;失败，错误信息：&#123;&#123;message&#125;&#125;</code></li></ul></li><li>再为上一步的<strong>选择器</strong>节点的<strong>否则</strong>添加一个<a href="https://www.coze.cn/store/plugin/7454184307662405659"><strong>小红书的get_url节点</strong></a>，其输入变量<code>share_content</code>引用<strong>开始</strong>节点的<code>rednote_share</code></li><li>继续为<strong>get_url</strong>添加<a href="https://www.coze.cn/store/plugin/7425905322386096140"><strong>小红书连接处理的shoturl节点</strong></a>，其输入变量<code>short_url</code>引用上一步<strong>get_url</strong>节点的<code>url</code>输出字段</li><li>然后添加一个<a href="https://www.coze.cn/store/plugin/7492043573437792293"><strong>小红书笔记详情的GetNoteData节点</strong></a>，其输入变量<code>url</code>引用上一步<strong>shoturl</strong>节点的<code>long_url</code>输出字段</li><li>接下来创建一个<strong>代码</strong>节点，这是整个工作流的核心<ul><li>添加如下输入变量：<ul><li><code>title</code>引用<strong>GetNoteData</strong>的<code>title</code>输出字段</li><li><code>content</code>引用<strong>GetNoteData</strong>的<code>content</code>输出字段</li><li><code>images</code>引用<strong>GetNoteData</strong>的<code>images</code>输出字段</li><li><code>tags</code>引用<strong>GetNoteData</strong>的<code>tags</code>输出字段</li><li><code>parent_id</code>引用用户变量<code>notion_page_root</code></li><li><code>secret</code>引用用户变量<code>notion_secret</code></li></ul></li><li>添加一个类型为<code>Object</code>的输出变量<code>response</code>，并添加如下输出字段<ul><li><code>status</code>，类型为<code>Integer</code></li><li><code>message</code>，类型为<code>String</code></li><li><code>url</code>，类型为<code>String</code></li></ul></li><li>点击代码节点的<code>在IDE中编辑</code>，清空内容并粘贴如下代码片段（我知道，这代码很不Pythonic）</li></ul></li></ul><figure class="highlight python"><table><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><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> tempfile</span><br><span class="line"><span class="keyword">import</span> requests_async <span class="keyword">as</span> requests</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">main</span>(<span class="params">args: Args</span>) -&gt; Output:</span><br><span class="line">    params = args.params</span><br><span class="line"></span><br><span class="line">    parent_id = params[<span class="string">&#x27;parent_id&#x27;</span>]</span><br><span class="line">    secret = params[<span class="string">&#x27;secret&#x27;</span>]</span><br><span class="line">    title = params[<span class="string">&#x27;title&#x27;</span>]</span><br><span class="line">    content = params[<span class="string">&#x27;content&#x27;</span>]</span><br><span class="line">    images = params[<span class="string">&#x27;images&#x27;</span>]</span><br><span class="line">    tags = params[<span class="string">&#x27;tags&#x27;</span>]</span><br><span class="line"></span><br><span class="line">    file_upload_id_list = <span class="keyword">await</span> transfer_images(secret, images)</span><br><span class="line">    post = build_post(parent_id, title, content, file_upload_id_list, tags)</span><br><span class="line">    create_post_response = <span class="keyword">await</span> create_post(secret, post)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 构建输出对象</span></span><br><span class="line">    ret: Output = &#123;</span><br><span class="line">        <span class="string">&quot;response&quot;</span>: create_post_response</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ret</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">transfer_images</span>(<span class="params">secret: <span class="built_in">str</span>, images: <span class="built_in">list</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="built_in">list</span>[<span class="built_in">str</span>]:</span><br><span class="line">    file_upload_id_list = []</span><br><span class="line">    <span class="keyword">for</span> image <span class="keyword">in</span> images:</span><br><span class="line">        tmp_file_path = <span class="keyword">await</span> download_image_file(image)</span><br><span class="line">        file_upload_id = <span class="keyword">await</span> upload_file_to_notion(secret, tmp_file_path)</span><br><span class="line">        file_upload_id_list.append(file_upload_id)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> file_upload_id_list</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">download_image_file</span>(<span class="params">url: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    tmp_file = tempfile.NamedTemporaryFile(delete=<span class="literal">False</span>, suffix=<span class="string">&#x27;.jpg&#x27;</span>)</span><br><span class="line">    tmp_file_path = tmp_file.name</span><br><span class="line">    tmp_file.close()</span><br><span class="line"></span><br><span class="line">    response = <span class="keyword">await</span> requests.get(url)</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(tmp_file_path, <span class="string">&#x27;wb&#x27;</span>) <span class="keyword">as</span> file:</span><br><span class="line">        file.write(response.content)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> tmp_file_path</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">upload_file_to_notion</span>(<span class="params">secret: <span class="built_in">str</span>, file_path: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="comment"># Initiate file upload</span></span><br><span class="line">    url = <span class="string">&#x27;https://api.notion.com/v1/file_uploads&#x27;</span></span><br><span class="line">    response = <span class="keyword">await</span> requests.post(url, headers=&#123;</span><br><span class="line">        <span class="string">&#x27;Authorization&#x27;</span>: <span class="string">&#x27;Bearer &#x27;</span> + secret,</span><br><span class="line">        <span class="string">&#x27;Notion-Version&#x27;</span>: <span class="string">&#x27;2022-06-28&#x27;</span>,</span><br><span class="line">    &#125;, data=&#123;</span><br><span class="line">        <span class="string">&#x27;mode&#x27;</span>: <span class="string">&#x27;single_part&#x27;</span>,</span><br><span class="line">    &#125;)</span><br><span class="line">    file_upload_id = response.json()[<span class="string">&#x27;id&#x27;</span>]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Upload the file</span></span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(file_path, <span class="string">&#x27;rb&#x27;</span>) <span class="keyword">as</span> file:</span><br><span class="line">        files =  &#123;</span><br><span class="line">            <span class="string">&#x27;file&#x27;</span>: (os.path.basename(file_path), file, <span class="string">&#x27;image/jpeg&#x27;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        url = <span class="string">f&#x27;https://api.notion.com/v1/file_uploads/<span class="subst">&#123;file_upload_id&#125;</span>/send&#x27;</span></span><br><span class="line">        <span class="keyword">await</span> requests.post(url, headers=&#123;</span><br><span class="line">            <span class="string">&#x27;Authorization&#x27;</span>: <span class="string">&#x27;Bearer &#x27;</span> + secret,</span><br><span class="line">            <span class="string">&#x27;Notion-Version&#x27;</span>: <span class="string">&#x27;2022-06-28&#x27;</span>,</span><br><span class="line">        &#125;, files=files)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> file_upload_id</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">build_post</span>(<span class="params">parent_id: <span class="built_in">str</span>, title: <span class="built_in">str</span>, content: <span class="built_in">str</span>, file_ids: <span class="built_in">list</span>[<span class="built_in">str</span>], tags: <span class="built_in">list</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="built_in">object</span>:</span><br><span class="line">    content_blocks = build_content_blocks(content)</span><br><span class="line">    </span><br><span class="line">    files = []</span><br><span class="line">    <span class="keyword">for</span> file_id <span class="keyword">in</span> file_ids:</span><br><span class="line">        files.append(&#123;</span><br><span class="line">            <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;file_upload&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;file_upload&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;id&#x27;</span>: file_id</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="string">&#x27;parent&#x27;</span>: &#123;</span><br><span class="line">            <span class="string">&#x27;database_id&#x27;</span>: parent_id</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&#x27;properties&#x27;</span>: &#123;</span><br><span class="line">            <span class="string">&#x27;Name&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;title&#x27;</span>: [</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="string">&#x27;text&#x27;</span>: &#123;</span><br><span class="line">                            <span class="string">&#x27;content&#x27;</span>: title</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="string">&#x27;Images&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;files&#x27;</span>: files</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="string">&#x27;Tags&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;multi_select&quot;</span>: build_tag_properties(tags)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&#x27;children&#x27;</span>: content_blocks</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">build_content_blocks</span>(<span class="params">content: <span class="built_in">str</span></span>) -&gt; <span class="built_in">list</span>[<span class="built_in">object</span>]:</span><br><span class="line">    blocks = [];</span><br><span class="line">    paragraphs = content.replace(<span class="string">&#x27;\t&#x27;</span>, <span class="string">&#x27;&#x27;</span>).split(<span class="string">&#x27;\n&#x27;</span>)</span><br><span class="line">    <span class="keyword">for</span> paragraph <span class="keyword">in</span> paragraphs:</span><br><span class="line">        blocks.append(&#123;</span><br><span class="line">            <span class="string">&#x27;object&#x27;</span>: <span class="string">&#x27;block&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;paragraph&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;paragraph&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;rich_text&#x27;</span>: [</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;text&#x27;</span>,</span><br><span class="line">                        <span class="string">&#x27;text&#x27;</span>: &#123;</span><br><span class="line">                            <span class="string">&#x27;content&#x27;</span>: paragraph</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> blocks;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">build_tag_properties</span>(<span class="params">tags: <span class="built_in">list</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="built_in">list</span>[<span class="built_in">object</span>]:</span><br><span class="line">    items = []</span><br><span class="line">    <span class="keyword">for</span> tag <span class="keyword">in</span> tags:</span><br><span class="line">        items.append(&#123;</span><br><span class="line">            <span class="string">&quot;name&quot;</span>: tag</span><br><span class="line">        &#125;)</span><br><span class="line">    <span class="keyword">return</span> items</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">create_post</span>(<span class="params">secret: <span class="built_in">str</span>, post: <span class="built_in">object</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    url = <span class="string">&#x27;https://api.notion.com/v1/pages&#x27;</span></span><br><span class="line">    response = <span class="keyword">await</span> requests.post(url, headers=&#123;</span><br><span class="line">        <span class="string">&#x27;Authorization&#x27;</span>: <span class="string">&#x27;Bearer &#x27;</span> + secret,</span><br><span class="line">        <span class="string">&#x27;Notion-Version&#x27;</span>: <span class="string">&#x27;2022-06-28&#x27;</span>,</span><br><span class="line">    &#125;, json=post)</span><br><span class="line">    <span class="keyword">return</span> response.json()</span><br></pre></td></tr></table></figure><ul><li>为<strong>代码</strong>节点添加一个<strong>选择器</strong>节点，判断<strong>代码</strong>节点的输出字段<code>status</code>是否为<code>200</code>，并分别为其添加如下后续节点<ul><li><strong>如果</strong>部分添加输出节点，添加一个输入变量<code>output</code>，引用<strong>代码</strong>节点的<code>url</code>输出字段，输出内容为<code>&#123;&#123;output&#125;&#125;</code>，负责在帖子创建成功后输出帖子URL</li><li><strong>否则</strong>部分添加输出节点，添加一个输入变量<code>output</code>，引用<strong>代码</strong>节点的<code>message</code>输出字段，输出内容为<code>&#123;&#123;output&#125;&#125;</code>，负责在帖子创建失败后输出错误信息</li></ul></li><li>添加一个<strong>变量聚合</strong>节点并关联<strong>连接Notion失败的输出节点</strong>、<strong>创建帖子成功的URL输出节点</strong>、<strong>创建帖子失败的错误信息输出节点</strong>，其<code>Group1</code>的配置参考下图<br><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/coze_workflow_variable_aggregate.png"></li><li>最后为<strong>变量聚合节点</strong>添加一个<strong>结束</strong>节点，添加一个输出变量<code>output</code>引用<strong>变量聚合节点</strong>的<code>Group1</code>输出字段</li></ul><p>如果最后你的工作流与我上面的截图一致，那么就大致可以认为你的工作流配置是正确的了。接下来就可以点击<code>试运行</code>按钮，贴一个小红书分享链接，来测试这个工作流。一切正常的话，工作流会输出一个Notion的URL，同时你在Notion的这个数据库中就能看见刚刚转存过来的帖子。</p><p><img data-src="https://blog-static.boris1993.com/save_rednote_to_notion_with_coze_workflow/notion_saved_post_example.png"></p><h2 id="接下来的计划"><a href="#接下来的计划" class="headerlink" title="接下来的计划"></a>接下来的计划</h2><p>目前这个工作流虽然实现了转存小红书笔记的功能，但仍有下面几个问题：</p><ul><li>小红书的cookie不会自动刷新，那么在cookie过期后，整个工作流就不能正常运行，需要用户手动去浏览器拷贝小红书的cookie并进入智能体配置页面更新</li><li>在手机上不能很方便的调用这个工作流，目前只能进入到工作流编辑页面，通过试运行的方式调用</li><li>Notion中的帖子不会去重，意味着如果我反复转存同一篇笔记，那么Notion中就会出现多个一模一样的帖子</li><li>小红书解析相关功能依赖别人提供的插件，而这些插件有被下架的风险</li></ul><p>所以接下来我计划做两件事：</p><ul><li>一个Chrome浏览器插件，它可以定时刷新小红书页面并截获cookie，然后调用扣子的API更新对应的变量值</li><li>一个基于iOS的Scriptable应用的脚本，使其支持系统的分享功能，它将可以调用扣子的API来调用这个工作流，并将小红书分享链接作为参数传入</li></ul><p>至于帖子重复的问题我不打算用代码的方式解决，因为收益实在太低了，就算重复了，也无非手动去Notion里删一下的事。插件被下架的风险暂时也不打算考虑，现有的能用就先用着，哪天不能用了再说。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Docker中的PID 1和tini：为什么你的容器不响应Ctrl-C</title>
      <link>https://www.boris1993.com/translation-docker-pid-1-and-tini.html</link>
      <description>
        <![CDATA[<p>之前我写过一篇文，讲我是怎么<a href="/finding-the-docker-container-of-a-zombie-process.html">处置Docker容器产生的僵尸进程的</a>，正巧前两天上网乱刷，看到有个人也被容器中的僵尸进程困扰，有一条回复提到了一个关键词<code>tini</code>，说能根治这个问题，于是继续上网冲浪，翻到了<span class="exturl" data-url="aHR0cHM6Ly9kZXYtYWRpdHlhLm1lZGl1bS5jb20vcGlkLTEtYW5kLXRpbmktaW4tZG9ja2VyLXdoeS15b3VyLWNvbnRhaW5lci1pZ25vcmVzLWN0cmwtYy04MDBiNTY1Y2I3NmU=">Medium上的这篇文章<i class="fa fa-external-link-alt"></i></span>，感觉很有用，所以翻译出来。</p>
<p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失）负责。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/translations/">翻译</category>
      <category domain="https://www.boris1993.com/tags/Docker/">Docker</category>
      <category domain="https://www.boris1993.com/tags/tini/">tini</category>
      <pubDate>Sat, 07 Jun 2025 00:40:16 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>之前我写过一篇文，讲我是怎么<a href="/finding-the-docker-container-of-a-zombie-process.html">处置Docker容器产生的僵尸进程的</a>，正巧前两天上网乱刷，看到有个人也被容器中的僵尸进程困扰，有一条回复提到了一个关键词<code>tini</code>，说能根治这个问题，于是继续上网冲浪，翻到了<span class="exturl" data-url="aHR0cHM6Ly9kZXYtYWRpdHlhLm1lZGl1bS5jb20vcGlkLTEtYW5kLXRpbmktaW4tZG9ja2VyLXdoeS15b3VyLWNvbnRhaW5lci1pZ25vcmVzLWN0cmwtYy04MDBiNTY1Y2I3NmU=">Medium上的这篇文章<i class="fa fa-external-link-alt"></i></span>，感觉很有用，所以翻译出来。</p><p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失）负责。</p><span id="more"></span><p>在使用Docker的时候，你有可能会遇到这么一种很难受的情况，就是你敲了Ctrl-C想停掉这个容器，但这个容器却无动于衷。或者又可能你的容器停止了，但留下了一堆僵尸进程。这些问题通常来自于一个开发者们从一开始就没想明白的问题 —— 如果你的程序成为了容器中的PID 1会怎么样。</p><h2 id="什么是PID-1"><a href="#什么是PID-1" class="headerlink" title="什么是PID 1"></a>什么是PID 1</h2><p>在Linux系统中，PID 1（进程号1）是在系统启动过程中第一个启动的进程。它扮演着一个特殊的角色，即系统的初始化者。它将负责启动和管理所有其他的进程。在Docker容器中，你启动的第一个进程将默认成为PID 1。</p><p>例如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -it node:18 node</span><br></pre></td></tr></table></figure><p>这时候，Node.js的进程就是PID 1。</p><h2 id="为什么PID-1很特别？"><a href="#为什么PID-1很特别？" class="headerlink" title="为什么PID 1很特别？"></a>为什么PID 1很特别？</h2><p>PID 1的进程在Linux中会有如下几种特殊的行为：</p><ol><li>响应信号的方式不同<br>大多数UNIX进程会自动接收并处理类似<code>SIGINT</code>（来自Ctrl-C）和<code>SIGTERM</code>（来自<code>docker stop</code>命令）的信号。<br>但是PID 1的进程默认不会接收这些信号，除非它们主动监听。</li><li>收割僵尸进程<br>如果PID 1不等待子进程，那么这些子进程就会变成僵尸。尽管它们已经退出了，但仍然会消耗系统资源。（译者注：我理解就是在容器停止的时候，主进程不等待它的子进程全部退出成功再退出，而是就自己拍拍屁股走人了）<br>这会慢慢地拖慢这个容器的性能，或让这个容器的行为变得失控。</li></ol><h2 id="问题示例"><a href="#问题示例" class="headerlink" title="问题示例"></a>问题示例</h2><p>假设我们有这样一个简单的Node.js应用：</p><figure class="highlight javascript"><table><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"><span class="comment">// app.js</span></span><br><span class="line"><span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Running...&#x27;</span>), <span class="number">1000</span>);</span><br></pre></td></tr></table></figure><p>这个应用会运行在一个Docker容器中：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -it node:18 node app.js</span><br></pre></td></tr></table></figure><p>如果此时你尝试使用Ctrl-C退出，那么什么都不会发生。很奇怪对吧？因为：</p><ul><li>Node.js在容器中是PID 1进程</li><li>当它是PID 1的时候，它没有正确转发或监听<code>SIGINT</code>信号</li></ul><p>最终，你可能会一边纳闷为啥Ctrl-C不好使，一边反复敲它。这时候就是<code>tini</code>出场的时候了。</p><h2 id="tini：轻量的init"><a href="#tini：轻量的init" class="headerlink" title="tini：轻量的init"></a>tini：轻量的init</h2><p><code>tini</code>是个简化的初始化（init）系统，体积只有几KB，并且专为容器环境设计。Docker甚至集成了它，你只需要用<code>--init</code>参数就能开启。</p><h2 id="tini负责干什么？"><a href="#tini负责干什么？" class="headerlink" title="tini负责干什么？"></a>tini负责干什么？</h2><ol><li>信号转发<br>它会监听类似<code>SIGINT</code>和<code>SIGTERM</code>之类的信号，并正确地将其转发到你的应用程序。这样一来，Ctrl-C或<code>docker stop</code>就会按照预期工作了。</li><li>收割僵尸进程<br><code>tini</code>会收割死亡的子进程，这样它们就不会变成僵尸了。</li><li>就像一个负责任的PID 1一样干活<br>它工作起来就像一个真正的Linux初始化系统，只是更小了点。</li></ol><h2 id="怎么用tini？"><a href="#怎么用tini？" class="headerlink" title="怎么用tini？"></a>怎么用tini？</h2><h3 id="方法1-使用Docker内置的功能"><a href="#方法1-使用Docker内置的功能" class="headerlink" title="方法1:使用Docker内置的功能"></a>方法1:使用Docker内置的功能</h3><p>Docker在引擎内部已经集成了<code>tini</code>，所以你在运行容器时加上<code>--init</code>参数，那么Docker就会自动使用<code>tini</code>作为容器内的PID 1进程。你不需要额外安装或配置什么东西，只需要在命令中加上<code>--init</code>，就像这样：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --init -it node:18 node app.js</span><br></pre></td></tr></table></figure><h3 id="方法2-在Dockerfile中手动添加tini"><a href="#方法2-在Dockerfile中手动添加tini" class="headerlink" title="方法2:在Dockerfile中手动添加tini"></a>方法2:在Dockerfile中手动添加tini</h3><p>你也可以显式地为<code>Dockerfile</code>添加<code>tini</code>：</p><figure class="highlight dockerfile"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> node:<span class="number">18</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装tini</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; apt-get install -y tini</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 使tini成为入口点</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">&quot;/usr/bin/tini&quot;</span>, <span class="string">&quot;--&quot;</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行你的应用</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;node&quot;</span>, <span class="string">&quot;app.js&quot;</span>]</span></span><br></pre></td></tr></table></figure><p>然后你可以像往常一样构建和运行它：</p><figure class="highlight shell"><table><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">docker build -t node-app .</span><br><span class="line">docker run -it node-app</span><br></pre></td></tr></table></figure><h2 id="为什么在生产环境会很重要？"><a href="#为什么在生产环境会很重要？" class="headerlink" title="为什么在生产环境会很重要？"></a>为什么在生产环境会很重要？</h2><p>在生产环境（比如Kubernetes）中，无法处理信号可能会引发这些问题：</p><ul><li>应用程序无法干净的退出</li><li>数据因为处理停机的逻辑没有触发而导致损坏</li><li>僵尸进程造成资源泄漏</li></ul><p>使用<code>tini</code>就可以用最小的成本避免这些问题。</p><h2 id="最后的一点想法"><a href="#最后的一点想法" class="headerlink" title="最后的一点想法"></a>最后的一点想法</h2><p>尽管容器内部的行为很容易被忽略，但理解PID 1如何工作，以及<code>tini</code>解决了什么问题，能让你的容器变得更加干净、安全，并更容易维护。所以下次你遇到哪个应用不响应Ctrl-C，记得叫来小小的<code>tini</code>。</p><hr><p>译者注：如果你使用Docker Compose编排容器的话，那么在配置文件中指定<code>init: true</code>就可以引入<code>tini</code>了，就像这样：</p><figure class="highlight yaml"><table><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="attr">services:</span></span><br><span class="line">    <span class="attr">app:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">alpine:latest</span></span><br><span class="line">        <span class="attr">init:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>自己实现一个BlockingQueue并优化</title>
      <link>https://www.boris1993.com/enhancing-my-blocking-queue-implementation.html</link>
      <description>
        <![CDATA[<p>前阵看见个面试题</p>
<blockquote>
<p>请实现一个泛型类BlockingQueue：</p>
<ol>
<li>构造函数里指定队列容量</li>
<li>void put(T item) 队列满了会阻塞，直到队列有空间</li>
<li>T take()，队列空时会阻塞，直到队列有元素</li>
</ol>
</blockquote>
<p>我寻思做做看，结果写出来的答案我自己都看不下去，那干脆看看Java里面是咋实现的。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/Java/">Java</category>
      <category domain="https://www.boris1993.com/tags/BlockingQueue/">BlockingQueue</category>
      <pubDate>Wed, 26 Mar 2025 07:10:11 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>前阵看见个面试题</p><blockquote><p>请实现一个泛型类BlockingQueue：</p><ol><li>构造函数里指定队列容量</li><li>void put(T item) 队列满了会阻塞，直到队列有空间</li><li>T take()，队列空时会阻塞，直到队列有元素</li></ol></blockquote><p>我寻思做做看，结果写出来的答案我自己都看不下去，那干脆看看Java里面是咋实现的。</p><span id="more"></span><h2 id="我的实现"><a href="#我的实现" class="headerlink" title="我的实现"></a>我的实现</h2><p>先看看我写的玩意是啥德行吧。实话说这玩意看着都不像能正常工作的样子（虽然实际上它还真能用）</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlockingQueue</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Object[] items;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxSize;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">int</span> count;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">BlockingQueue</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> size)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.items = <span class="keyword">new</span> <span class="title class_">Object</span>[size];</span><br><span class="line">        <span class="built_in">this</span>.maxSize = size;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">put</span><span class="params">(<span class="keyword">final</span> T item)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="built_in">this</span>.count &gt;= maxSize) &#123;</span><br><span class="line">            wait();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.count &lt; maxSize) &#123;</span><br><span class="line">                notifyAll();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">synchronized</span> (<span class="built_in">this</span>) &#123;</span><br><span class="line">            items[count] = item;</span><br><span class="line">            count++;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">synchronized</span> T <span class="title function_">take</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">while</span> (count == <span class="number">0</span>) &#123;</span><br><span class="line">            wait();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (count &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                notifyAll();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">T</span> <span class="variable">item</span> <span class="operator">=</span> (T) items[<span class="number">0</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">1</span>; i &lt; count; i++) &#123;</span><br><span class="line">            items[i - <span class="number">1</span>] = items[i];</span><br><span class="line">        &#125;</span><br><span class="line">        count--;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> item;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码主要有俩问题，一个是这些<code>while</code>循环看着很不爽；另一个是在取出元素后逐个将元素左移的操作效率非常的低；而且满天飞的<code>wait()</code>和<code>notifyAll()</code>也会影响代码的可读性。</p><p>那么接下来看看这段代码怎么优化成接近Java自己的实现。</p><h2 id="优化阻塞"><a href="#优化阻塞" class="headerlink" title="优化阻塞"></a>优化阻塞</h2><p>BlockingQueue的一个机制是，在队列空时取元素的操作会被阻塞，而在队列满时放元素的操作会被阻塞。在Java的<code>ArrayBlockingQueue</code>中，它并没有使用<code>synchronized</code>加锁，而是用了<code>ReentrantLock</code>对象。此外对于阻塞线程和唤醒线程操作，它也没有用<code>wait()</code>和<code>notifyAll()</code>，而是通过两个<code>Condition</code>对象<code>notEmpty</code>和<code>notFull</code>实现。</p><p>所以改进方案就是，去掉方法定义中的<code>synchronized</code>关键字，换成用<code>ReentrantLock</code>加锁解锁；去掉<code>wait()</code>和<code>notifyAll()</code>，改成用<code>notEmpty</code>和<code>notFull</code>管理状态以及负责唤醒，同时可以借助这两个变量的语义增强代码的可读性。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlockingQueue</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Object[] items;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxSize;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReentrantLock lock;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Condition notEmpty;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Condition notFull;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">BlockingQueue</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> size)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.items = <span class="keyword">new</span> <span class="title class_">Object</span>[size];</span><br><span class="line">        <span class="built_in">this</span>.maxSize = size;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.lock = <span class="keyword">new</span> <span class="title class_">ReentrantLock</span>();</span><br><span class="line">        <span class="built_in">this</span>.notEmpty = lock.newCondition();</span><br><span class="line">        <span class="built_in">this</span>.notFull = lock.newCondition();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">put</span><span class="params">(<span class="keyword">final</span> T item)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="comment">// 尝试上锁，使其线程安全</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">ReentrantLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="built_in">this</span>.lock;</span><br><span class="line">        lock.lockInterruptibly();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (count &gt;= maxSize) &#123;</span><br><span class="line">                <span class="comment">// 在队列满时使当前线程进入等待状态</span></span><br><span class="line">                <span class="comment">// 直到其signal()方法被调用</span></span><br><span class="line">                notFull.await();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 存入元素，更新元素数量</span></span><br><span class="line">            items[count] = item;</span><br><span class="line">            count++;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 通知此时队列非空，唤醒在调用take()方法时因队列空而被阻塞的线程</span></span><br><span class="line">            notEmpty.signal();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 最后无论如何要保证锁被释放</span></span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> T <span class="title function_">take</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="comment">// 尝试上锁，使其线程安全</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">ReentrantLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="built_in">this</span>.lock;</span><br><span class="line">        lock.lockInterruptibly();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (count == <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="comment">// 在队列空时使当前线程进入等待状态</span></span><br><span class="line">                <span class="comment">// 直到其signal()方法被调用</span></span><br><span class="line">                notEmpty.await();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 取出最早被放入的元素</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">T</span> <span class="variable">item</span> <span class="operator">=</span> (T) items[<span class="number">0</span>];</span><br><span class="line">            <span class="comment">// 领头的元素出列了，后面的往前挪一个位置</span></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">1</span>; i &lt; count; i++) &#123;</span><br><span class="line">                items[i - <span class="number">1</span>] = items[i];</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 更新元素数量</span></span><br><span class="line">            count--;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 通知此时队列非满，唤醒在调用put()方法时因队列满而被阻塞的线程</span></span><br><span class="line">            notFull.signal();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> item;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 最后无论如何要保证锁被释放</span></span><br><span class="line">            notFull.signal();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="优化取出元素后更新队列内容"><a href="#优化取出元素后更新队列内容" class="headerlink" title="优化取出元素后更新队列内容"></a>优化取出元素后更新队列内容</h2><p>如果这是一个非常大的队列，那么用一个for循环将所有元素往左移来更新队列内容的方式会消耗巨量的时间，显然它的效率是非常低的。相比于每次取出元素后都重排数组中的元素，Java中<code>ArrayBlockingQueue</code>则是利用循环数组的思路，通过两个指针来指示当前该从什么位置取或该向什么位置存（这不是经典的双指针玩法么，前段时间刷LeetCode还做过呢，咋就没想起来），这样每次存取元素后，只需要更新指针指向的位置就行，效率可想而知非常高。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlockingQueue</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Object[] items;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">takeIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">putIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReentrantLock lock;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Condition notEmpty;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Condition notFull;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">BlockingQueue</span><span class="params">(<span class="keyword">final</span> <span class="type">int</span> size)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.items = <span class="keyword">new</span> <span class="title class_">Object</span>[size];</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.lock = <span class="keyword">new</span> <span class="title class_">ReentrantLock</span>();</span><br><span class="line">        <span class="built_in">this</span>.notEmpty = lock.newCondition();</span><br><span class="line">        <span class="built_in">this</span>.notFull = lock.newCondition();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">put</span><span class="params">(<span class="keyword">final</span> T item)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">ReentrantLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="built_in">this</span>.lock;</span><br><span class="line">        lock.lockInterruptibly();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (count &gt;= items.length) &#123;</span><br><span class="line">                notFull.await();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            items[putIndex] = item;</span><br><span class="line">            count++;</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">if</span> (++putIndex == items.length) &#123;</span><br><span class="line">                putIndex = <span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            notEmpty.signal();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> T <span class="title function_">take</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">ReentrantLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="built_in">this</span>.lock;</span><br><span class="line">        lock.lockInterruptibly();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">while</span> (count == <span class="number">0</span>) &#123;</span><br><span class="line">                notEmpty.await();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">final</span> <span class="type">T</span> <span class="variable">item</span> <span class="operator">=</span> (T) items[takeIndex];</span><br><span class="line">            items[takeIndex] = <span class="literal">null</span>;</span><br><span class="line">            count--;</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">if</span> (++takeIndex == items.length) &#123;</span><br><span class="line">                takeIndex = <span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            notFull.signal();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> item;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            notFull.signal();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>防止MicroServer Gen 10 Plus的风扇突然狂飙</title>
      <link>https://www.boris1993.com/microserver-gen10-plus-prevent-fan-goes-crazy.html</link>
      <description>
        <![CDATA[<p>今年元旦我把之前用的Gen 8换成了Gen 10 Plus。后来想着现在性能上去了，可以把NAS的功能也合并进来，于是就把给NAS用的两块机械硬盘迁进了Gen 10 Plus里。但就在这之后，我发现它的风扇时不时就会突然狂转一分多钟，然后再降回平时的转速。而且这个问题不分白天黑夜，甚至有一次大半夜把我吵醒，不胜其烦。终于在上周，我找到了问题的根源，并彻底解决了。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/Gen-10-Plus/">Gen 10 Plus</category>
      <pubDate>Sat, 22 Mar 2025 22:14:39 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>今年元旦我把之前用的Gen 8换成了Gen 10 Plus。后来想着现在性能上去了，可以把NAS的功能也合并进来，于是就把给NAS用的两块机械硬盘迁进了Gen 10 Plus里。但就在这之后，我发现它的风扇时不时就会突然狂转一分多钟，然后再降回平时的转速。而且这个问题不分白天黑夜，甚至有一次大半夜把我吵醒，不胜其烦。终于在上周，我找到了问题的根源，并彻底解决了。</p><span id="more"></span><h2 id="一些无效的尝试"><a href="#一些无效的尝试" class="headerlink" title="一些无效的尝试"></a>一些无效的尝试</h2><blockquote><p>这一节并没有解决方案，如果你不关心我之前的心路历程，那么可以放心跳到下一节。</p></blockquote><p>最起初，我以为是机械硬盘发热导致的，毕竟问题是出在我把机械硬盘装进来之后。为此我还调换了硬盘的安装位置，把机械硬盘从3、4号盘位移到了靠上的1、3号盘位。</p><figure class="highlight plaintext"><table><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><br><span class="line">| 1 - SSD | 3 - HDD |    =&gt;    | 1 - HDD | 3 - HDD |</span><br><span class="line">| 2 - SSD | 4 - HDD |          | 2 - SSD | 4 - SSD |</span><br><span class="line">---------------------          ---------------------</span><br></pre></td></tr></table></figure><p>可惜，并没有任何好转。风扇依旧时不时开始跟李有田似的狂飙，唯一区别是风扇能刹住车（为李有田默哀一秒……</p><p>后来我觉得，会不会是硬盘在跑PT的时候真的太热了？但是我也在下载的时候摸过硬盘，那个温度撑死只能算温热，怎么想也不会造成整机过热啊？但是本着“能解决问题就行，哪怕再花点钱”的想法，我开始找用来替换的4TB 2.5寸SATA接口固态硬盘。结果……并不理想，现在京东自营只找到了三款。更难受的是，根据论坛上的赛博邻居反馈，其中两款是数据火葬场；而剩下的那款，一块盘要卖我两千多块，也就是说两块盘加起来比我买这台Gen 10 Plus都贵。所以，换硬盘这个方案也被否了。</p><p>正巧就在这个时候，它又开始狂飙。我想，iLO里面不是能看见温度吗？那赶紧看看是啥玩意这么热呗？这一看，才找到了问题的根源。</p><h2 id="问题根源及解决方案"><a href="#问题根源及解决方案" class="headerlink" title="问题根源及解决方案"></a>问题根源及解决方案</h2><p>趁着风扇狂飙之际，我进入了iLO的温度页面，发现有个传感器<code>12 - AHCI Max HD</code>的当前温度是50°C，而它的警告阈值是60°C，严重错误阈值是70°C。也就是说，这时候BIOS发现这个东西烫的要挂掉了，所以就要赶紧让风扇转起来散热。等了一会风扇慢下来之后，我发现这个传感器的值变成了35°C，而且怎么刷新都不变。</p><p>搁网上一顿冲浪之后找到了<span class="exturl" data-url="aHR0cHM6Ly9zdXBwb3J0LmhwZS5jb20vaHBlc2MvcHVibGljL2RvY0Rpc3BsYXk/ZG9jSWQ9YTAwMDgzMTU0ZW5fdXMmZG9jTG9jYWxlPWVuX1VT">惠普的一篇通告<i class="fa fa-external-link-alt"></i></span>，从这里得知这并不是一个物理上的传感器，它检测到的温度是通过惠普的一个叫<code>Agentless Management Service</code>的软件组件向主板汇报的。此外还找到<span class="exturl" data-url="aHR0cHM6Ly93d3cuaGl0YWNoaS5jby5qcC9wcm9kdWN0cy9pdC9oYTgwMDB2L3N1cHBvcnQvcHJvZHVjdGluZm8vYXJ0aWNsZS5odG1sP2lkPUFEVi0yMDI0LTAwMTY=">日立的一个类似的通告<i class="fa fa-external-link-alt"></i></span>说硬盘在AHCI模式下会导致风扇转速异常升高。</p><p>到ESXi里面检查了之后，发现我用的惠普定制系统里面是带着AMS组件的。这时候我灵机一动，既然是因为AMS汇报了错误的温度才导致这个问题，而要让AMS汇报正确的温度，那我就得把硬盘换成HPE认证的盘，可想而知这玩意没准会相当贵，而且作为个人用户都不一定买得到。那如果我把AMS卸了，让传感器拿不到数据呢？</p><p>说干就干。保险起见我先去了惠普支持网站，下载好了最新版的AMS软件包，一旦出了问题我还可以把它再装回去。然后回到ESXi，开启SSH。</p><figure class="highlight bash"><table><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="comment"># 找到AMS的软件包名</span></span><br><span class="line">[root@ESXi:~] esxcli software vib list | grep ams</span><br><span class="line"><span class="comment"># 这个是给Gen 10系列用的</span></span><br><span class="line">amsd 701.11.8.5.22-1OEM.701.0.0.16850804 HPE VMwareAccepted 2023-08-08</span><br><span class="line"><span class="comment"># 这个是给Gen 11系列用的</span></span><br><span class="line">amsdv 701.11.3.0.17-1OEM.701.0.0.16850804 HPE VMwareAccepted 2023-06-12</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">[root@ESXi:~] esxcli software vib remove -n amsdv</span><br><span class="line">[root@ESXi:~] esxcli software vib remove -n amsd</span><br></pre></td></tr></table></figure><p>两条卸载命令都成功之后，逐个关掉虚拟机，然后重启ESXi使修改生效。果不其然，重启之后，iLO系统信息里面显示<code>Agentless Management Service</code>不可用，温度页面里<code>12 - AHCI HD Max</code>传感器也消失了。经过一星期的测试，风扇也确实没再狂飙过。问题解决！</p><h2 id="一点碎碎念"><a href="#一点碎碎念" class="headerlink" title="一点碎碎念"></a>一点碎碎念</h2><p>尽管这个解决方案远称不上完美，甚至有点“解决提出问题的人”的感觉。但这应该是权衡利弊之后的最优解。买HPE认证硬盘？拜托我只是拿它存电影而已，虽然要是全丢了也挺心疼，但它们确实不值这个钱。卖掉Gen 10 Plus再自己组一套NAS？那又太折腾了，我实在懒得搞。而且就算夏天天气热起来之后，我毕竟也不是重度PT玩家，大部分时间硬盘都是空载的，我相信它不会变得很热，不会因为俩机械硬盘搞炸整台服务器。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>利用观察者模式实现动态刷新Bean中的配置</title>
      <link>https://www.boris1993.com/dynamically-refresh-config-with-observer-pattern.html</link>
      <description>
        <![CDATA[<p>公司有的项目里面还是采用把API key明文写在<code>application.properties</code>或者某个单独的<code>properties</code>文件来管理，这既不安全也不符合公司的规定。所以我通过利用观察者模式和Spring的事件机制，将其改为加密存储于配置中心，并且实现了在配置中心更新后，服务中生效的配置也可以立即更新。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/">设计模式</category>
      <category domain="https://www.boris1993.com/tags/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F/">观察者模式</category>
      <pubDate>Fri, 21 Mar 2025 06:14:56 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>公司有的项目里面还是采用把API key明文写在<code>application.properties</code>或者某个单独的<code>properties</code>文件来管理，这既不安全也不符合公司的规定。所以我通过利用观察者模式和Spring的事件机制，将其改为加密存储于配置中心，并且实现了在配置中心更新后，服务中生效的配置也可以立即更新。</p><span id="more"></span><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>谈论实现之前，我先说一下我有哪些工具可以使用：</p><ul><li>SecretKeyService：一个可以保存对称及非对称加密密钥的服务</li><li>CredentialVault：一个用来存放密钥的服务，密钥一旦存入，就只能再通过API的方式得到其原文</li><li>ConfigurationCenter：配置中心，不提供任何加密功能，在配置内容更新后可以通过Spring的事件通知到使用了这个配置的服务</li></ul><p>接到这个需求后，我下意识地觉得应该用CredentialVault解决问题，但是仔细想想，发现并不能。因为按照公司要求，API key的密码必须定期轮换，而老的密码在轮换之后就会马上失效。尽管我们有两套API key来避免前面的问题，但是更换API key密钥对又需要修改配置文件，并经历code review及上线部署，依旧需要一定的人工操作。我的设想是，只需要在一个地方设定好要生效的API key，接下来所有用到这个API key的服务都能自动更新。这样看来，似乎带有通知功能的配置中心是唯一解。但是配置中心不提供加密功能，所以还需要SecretKeyService提供一个对称加密密钥来把API key的密码加密，然后将它放到配置中心。</p><p>记不住这些名字没关系，后面你也看不到它们了。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>在确定要用的工具之后，就可以开始着手将它们拼装在一起了。因为原本公司的代码需要保密，所以下面的代码更多是展示思路，大概率你不能直接拷出来放到你的项目中用。</p><h3 id="PropertyLoader"><a href="#PropertyLoader" class="headerlink" title="PropertyLoader"></a>PropertyLoader</h3><p>因为直接把配置源从文件换成配置中心有一定的风险，保险起见我们决定逐步迁移配置，在迁移期间需要同时支持配置文件和配置中心。所以，就诞生了<code>PropertyLoader</code>。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PropertyLoader</span> &#123;</span><br><span class="line">    <span class="comment">// 负责加解密的服务，它会从SecretKeyService取得密钥并对给定字符串完成加解密</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> EncryptionDecryptionService encryptionDecryptionService;</span><br><span class="line">    <span class="comment">// 配置中心的客户端</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ConfigurationCenterClient configurationCenterClient;</span><br><span class="line">    <span class="comment">// 配置文件</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Properties properties;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">PropertyLoader</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> EncryptionDecryptionService encryptionDecryptionService,</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> ConfigurationCenterClient configurationCenterClient,</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> Properties properties,</span></span><br><span class="line"><span class="params">    )</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.encryptionDecryptionService = encryptionDecryptionService;</span><br><span class="line">        <span class="built_in">this</span>.configurationCenterClient = configurationCenterClient;</span><br><span class="line">        <span class="built_in">this</span>.properties = properties;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从配置中心或配置文件取得一个key的值</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key Property或配置中心条目的key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 取到的值，不会为null</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> PropertyNotFoundException 当在配置中心和配置文件中都找不到这个key时抛出，这通常意味着我们把某条配置漏掉了</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">loadProperty</span><span class="params">(<span class="keyword">final</span> String key)</span> &#123;</span><br><span class="line">        <span class="comment">// 首先尝试从配置中心取值，如果取不到则返回null</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> configurationCenterClient.getValue(key);</span><br><span class="line">        <span class="keyword">if</span> (value == <span class="literal">null</span>) &#123;</span><br><span class="line">            value = properties.getProperty(key);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (value == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">PropertyNotFoundException</span>(key);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从配置中心或配置文件取得一个key的值并将其解密</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key Property或配置中心条目的key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 取到的值，不会为null</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> PropertyNotFoundException 当在配置中心和配置文件中都找不到这个key时抛出，这通常意味着我们把某条配置漏掉了</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">loadEncryptedProperty</span><span class="params">(<span class="keyword">final</span> String key)</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">encryptedValue</span> <span class="operator">=</span> configurationCenterClient.getValue(key);</span><br><span class="line">        <span class="keyword">if</span> (encryptedValue == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 如果从配置中心拿不到值，那就从property文件中拿未加密的原文</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">String</span> <span class="variable">plainValue</span> <span class="operator">=</span> properties.getProperty(key);</span><br><span class="line">            <span class="keyword">if</span> (plainValue == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">PropertyNotFoundException</span>(key);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> plainValue;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> encryptionDecryptionService.decrypt(encryptedValue);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 检查配置文件或配置中心是否有指定的key</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 要检查的key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 当这个key存在时返回true，反之返回false</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">hasProperty</span><span class="params">(<span class="keyword">final</span> String key)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> configurationCenterClient.getKeys().contains(key) || properties.containsKey(key);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="实现观察者模式"><a href="#实现观察者模式" class="headerlink" title="实现观察者模式"></a>实现观察者模式</h3><p>上面说到了，我需要在监听到配置中心发出的事件后，更新相关对象中的配置。显然，在每个对象中都实现一个Spring事件监听器是很蠢的，我们应该在一处监听Spring事件，然后将其以某种方式广播到相关的对象。看起来，<span class="exturl" data-url="aHR0cHM6Ly9yZWZhY3RvcmluZy5ndXJ1L2Rlc2lnbi1wYXR0ZXJucy9vYnNlcnZlcg==">观察者模式<i class="fa fa-external-link-alt"></i></span>是个不错的选择。</p><p>观察者模式包含两个组件：<code>notifier</code>和<code>subject</code>。<code>Subject</code>作为观众，观察着某个事件；<code>notifier</code>则负责将事件通知给各个<code>subject</code>。</p><p>OK，理论有这些就够了。接下来我们把它实现。首先是<code>subject</code>。</p><h4 id="观察者模式-subject"><a href="#观察者模式-subject" class="headerlink" title="观察者模式 - subject"></a>观察者模式 - subject</h4><p>首选我们用一个抽象类定义一个观察者要有的行为，需要成为观察者的类将会继承这个抽象类。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">ConfigRefreshObserverSubject</span> &#123;</span><br><span class="line">    <span class="comment">// 观察到配置更新后，从PropertyLoader获取新的值</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> PropertyLoader propertyLoader;</span><br><span class="line">    <span class="comment">// 这个观察者关心的key</span></span><br><span class="line">    <span class="comment">// 因为要计算在这个对象中当前生效的配置的签名，所以要配置这个对象关心哪些key</span></span><br><span class="line">    <span class="comment">// 然后取出这些key对应的value来计算签名</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> List&lt;String&gt; monitoredKeys;</span><br><span class="line">    <span class="comment">// 当前生效的配置的一个签名</span></span><br><span class="line">    <span class="comment">// 可以将其理解为当前配置的一个哈希，实际上是综合当前生效的配置生成的一个UUID</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">private</span> String configSignature;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ConfigRefreshObserverSubject</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> PropertyLoader propertyLoader,</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> List&lt;String&gt; monitoredKeys</span></span><br><span class="line"><span class="params">    )</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.propertyLoader = propertyLoader;</span><br><span class="line">        <span class="built_in">this</span>.monitoredKeys = monitoredKeys;</span><br><span class="line">        <span class="comment">// 在初始化时计算当前生效配置的签名</span></span><br><span class="line">        <span class="built_in">this</span>.configSignature = calculateConfigurationSignature();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 这个方法留给实际成为subject的类去实现具体它要怎么刷新自己的配置</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> <span class="keyword">void</span> <span class="title function_">refreshConfigImpl</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">     * 这个方法留给notifier调用，实现更新配置及刷新配置签名</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">refreshConfig</span><span class="params">()</span> &#123;</span><br><span class="line">        refreshConfigImpl();</span><br><span class="line">        configSignature = calculateConfigurationSignature();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> PropertyLoader <span class="title function_">getPropertyLoader</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.propertyLoader;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getConfigSignature</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.configSignature;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> String <span class="title function_">calculateConfigurationSignature</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">StringBuilder</span> <span class="variable">newConfigurationSignatureSeedBuilder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">        <span class="keyword">for</span> (String key : monitoredKeys) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!propertyLoader.hasProperty(key)) &#123;</span><br><span class="line">                <span class="comment">// 如果找不到某个关心的key，那么说明要么初始配置有问题，要么刷新的配置有问题</span></span><br><span class="line">                <span class="comment">// 这时候尽早抛出异常引发开发人员关注</span></span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;Missing property: &quot;</span> + key + <span class="string">&quot; for class: &quot;</span> + <span class="built_in">this</span>.getClass().getSimpleName());</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            newConfigurationSignatureSeedBuilder.append(propertyLoader.loadProperty(key));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> UUID.nameUUIDFromBytes(newConfigurationSignatureSeedBuilder.toString().getBytes()).toString();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="观察者模式-notifier"><a href="#观察者模式-notifier" class="headerlink" title="观察者模式 - notifier"></a>观察者模式 - notifier</h4><p>在观察者模式中，<code>notifier</code>将作为一个单例存在，各个<code>subject</code>会注册到这个<code>notifier</code>，并在接收到事件后被<code>notifier</code>逐个通知。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ConfigRefreshObserverNotifier</span> &#123;</span><br><span class="line">    <span class="comment">// 用于存放各个subject的注册表</span></span><br><span class="line">    <span class="comment">// 在某些情况下，同一个subject可能会被重复注册，所以这里我会把subject的类名作为key放在map中</span></span><br><span class="line">    <span class="comment">// 方便在注册时检查是否重复注册</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, ConfigRefreshObserverSubject&gt; subjects = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="title function_">ConfigRefreshObserverNotifier</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// notifier作为一个单例，我们不希望它被实例化</span></span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>(<span class="string">&quot;ConfigRefreshObserverNotifier shouldn&#x27;t be instantiated&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 注册subject</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> subject 待注册的subject对象</span></span><br><span class="line"><span class="comment">     */</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_">register</span><span class="params">(<span class="keyword">final</span> ConfigRefreshObserverSubject subject)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (!isSubjectRegistered(subject)) &#123;</span><br><span class="line">            subjects.put(subject.getClass().getSimpleName(), subject);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 向各个subject发出配置更新的通知</span></span><br><span class="line"><span class="comment">     */</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_">notifyObservers</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">final</span> ConfigRefreshObserverSubject subject : subjects.values()) &#123;</span><br><span class="line">            <span class="comment">// 计算新配置的签名</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">String</span> <span class="variable">newConfigSignature</span> <span class="operator">=</span> subject.calculateConfigurationSignature();</span><br><span class="line">            <span class="comment">// 仅当签名不同，即配置有变化时，才通知对应的subject更新</span></span><br><span class="line">            <span class="keyword">if</span> (!newConfigSignature.equals(subject.getConfigSignature())) &#123;</span><br><span class="line">                subject.refreshConfig();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isSubjectRegistered</span><span class="params">(<span class="keyword">final</span> ConfigRefreshObserverSubject subject)</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">subjectClassName</span> <span class="operator">=</span> subject.getClass().getSimpleName();</span><br><span class="line">        <span class="keyword">return</span> subjects.containsKey(subjectClassName);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="观察者模式-实际的观察者类"><a href="#观察者模式-实际的观察者类" class="headerlink" title="观察者模式 - 实际的观察者类"></a>观察者模式 - 实际的观察者类</h4><p>有了<code>subject</code>，接下来就可以让实际要监控配置更新的类继承<code>ConfigRefreshObserverSubject</code>，将它变为一个观察者，并实现更新配置的逻辑。这部分其实很简单，就是给对应的字段重新赋值。</p><figure class="highlight java"><table><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_">SomeServiceClient</span> <span class="keyword">extends</span> <span class="title class_">ConfigRefreshObserverSubject</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String endpoint;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">SomeServiceClient</span><span class="params">(<span class="keyword">final</span> PropertyLoader propertyLoader)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(</span><br><span class="line">            propertyLoader,</span><br><span class="line">            List.of(<span class="string">&quot;some-service-endpoint&quot;</span>, <span class="string">&quot;some-service-username&quot;</span>, <span class="string">&quot;some-service-password&quot;</span>)</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        initializeProperties();</span><br><span class="line">        ConfigRefreshObserverNotifier.register(<span class="built_in">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">initializeProperties</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">PropertyLoader</span> <span class="variable">propertyLoader</span> <span class="operator">=</span> <span class="built_in">super</span>.getPropertyLoader();</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.endpoint = propertyLoader.loadProperty(<span class="string">&quot;some-service-endpoint&quot;</span>);</span><br><span class="line">        <span class="built_in">this</span>.username = propertyLoader.loadProperty(<span class="string">&quot;some-service-username&quot;</span>);</span><br><span class="line">        <span class="built_in">this</span>.password = propertyLoader.loadEncryptedProperty(<span class="string">&quot;some-service-password&quot;</span>);</span><br><span class="line">    &#125;</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_">refreshConfigImpl</span><span class="params">()</span> &#123;</span><br><span class="line">        initializeProperties();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 实际这个类的业务实现与本文无关，就省略了</span></span><br><span class="line">    <span class="comment">// 说白了无非就是往endpoint发请求，带上username和password来认证</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码应该很容易理解，在配置刷新前，<code>SomeServiceClient</code>就用当前生效的配置去发请求，在配置刷新后，这个观察者就可以马上得知这个事件并从配置中心取得最新的值替换当前生效的配置。这个过程几乎是瞬间完成的，不会对业务产生影响。</p><h3 id="监听配置刷新事件"><a href="#监听配置刷新事件" class="headerlink" title="监听配置刷新事件"></a>监听配置刷新事件</h3><p>上面洋洋洒洒实现了一堆东西，但最重要的一个还没有实现，那就是配置刷新事件的监听器。因为配置中心会通过Spring事件来发布，所以只需要找个地方实现一个<code>@EventListener</code>方法就行。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BeanFactory</span> &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> SomeServiceClient <span class="title function_">someServiceClient</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> ResourceLoader resourceLoader,</span></span><br><span class="line"><span class="params">        <span class="keyword">final</span> EncryptionDecryptionService encryptionDecryptionService </span></span><br><span class="line"><span class="params">    )</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">Properties</span> <span class="variable">properties</span> <span class="operator">=</span> ResourcesUtils.loadProperties(</span><br><span class="line">            resourceLoader,</span><br><span class="line">            ResourcesUtils.CLASSPATH_META_INF + <span class="string">&quot;/some-service.properties&quot;</span></span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">PropertyLoader</span> <span class="variable">propertyLoader</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PropertyLoader</span>(encryptionDecryptionService, properties);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SomeServiceClient</span>(propertyLoader);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@EventListener</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleConfigRefreshEvent</span><span class="params">(<span class="keyword">final</span> ConfigRefreshEvent event)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (event.getProjects().contains(<span class="string">&quot;my-config-project&quot;</span>)) &#123;</span><br><span class="line">            ConfigRefreshObserverNotifier.notifyObservers();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上，就完成了一个利用配置中心的通知机制实现的配置动态更新功能。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>深入了解Redis的Pub/Sub</title>
      <link>https://www.boris1993.com/translation-redis-pubsub-in-depth.html</link>
      <description>
        <![CDATA[<p>看到一篇深入讲解Redis中Pub&#x2F;Sub的文章<span class="exturl" data-url="aHR0cHM6Ly9tZWRpdW0uY29tL0Bqb3Vkd2F3YWQvcmVkaXMtcHViLXN1Yi1pbi1kZXB0aC1kMmM2ZjQzMzQ4MjY=">Redis Pub&#x2F;Sub In-Depth<i class="fa fa-external-link-alt"></i></span>，所以打算将它翻译出来，顺便深化自己的理解。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/translations/">翻译</category>
      <category domain="https://www.boris1993.com/tags/Redis/">Redis</category>
      <category domain="https://www.boris1993.com/tags/Pub-Sub/">Pub/Sub</category>
      <pubDate>Sat, 15 Mar 2025 01:18:06 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>看到一篇深入讲解Redis中Pub&#x2F;Sub的文章<span class="exturl" data-url="aHR0cHM6Ly9tZWRpdW0uY29tL0Bqb3Vkd2F3YWQvcmVkaXMtcHViLXN1Yi1pbi1kZXB0aC1kMmM2ZjQzMzQ4MjY=">Redis Pub&#x2F;Sub In-Depth<i class="fa fa-external-link-alt"></i></span>，所以打算将它翻译出来，顺便深化自己的理解。</p><span id="more"></span><p>Pub&#x2F;Sub（即publish&#x2F;subscribe的简称）是一个在分布式系统中给不同组件互相通信的一种消息传递技术。这种消息传递技术与传统的点对点通信（即一个服务直接向另一个服务发送消息）不同，它是一种异步且可伸缩的消息服务，并且它可以将负责发布消息的服务与负责处理消息的服务分隔开。</p><p>在这篇博客中，我们会探索Pub&#x2F;Sub的原理，以及Redis是如何实现这个通信模型的。我们会分析Redis中错综复杂的实现，将目光聚焦并深入到内存级别的实现的细节上，来让我们完全理解Pub&#x2F;Sub机制，以及通过Redis完成的项目实践。</p><h2 id="Pub-Sub入门"><a href="#Pub-Sub入门" class="headerlink" title="Pub&#x2F;Sub入门"></a>Pub&#x2F;Sub入门</h2><p>Pub&#x2F;Sub是一个消息模型，它可以让分布式系统中的不同组件互相通信。发布者向一个主题（topic）发送信息，订阅者则从这个主题中接收信息。发送者在这个过程中可以保持匿名，当然如果在消息中包含了发送者的信息，那么订阅者也可以通过它来确定发送者的身份。Pub&#x2F;Sub系统保证了消息可以被送达到所有对这个主题感兴趣的订阅者。在恰当的配置下，这将会是一个高度可扩展并且很可靠的消息传递系统，它将可以处理大量的数据。此外，在适当的大小、网络状况，以及订阅者的处理时间的前提下，Pub&#x2F;Sub允许服务以1毫秒的延迟进行异步通信，因此它非常适合现代快速的分布式应用程序。</p><h2 id="Pub-Sub模型"><a href="#Pub-Sub模型" class="headerlink" title="Pub&#x2F;Sub模型"></a>Pub&#x2F;Sub模型</h2><p>Pub&#x2F;Sub的模型很简单，消息中介从发布者收到消息，然后将消息分发到各个订阅者。订阅者在得到消息后，就可以根据实际的场景对消息进行处理。基于发布者和订阅者的数量，这个模型通常可以被归为四类，即一对一、一对多、多对一、多对多。</p><table><thead><tr><th>Pub&#x2F;Sub类型</th><th>描述</th></tr></thead><tbody><tr><td>一对一</td><td>包括一个发布者和一个订阅者。消息从发布者直接发送到订阅者。</td></tr><tr><td>一对多</td><td>包括一个发布者和多个订阅者。发布者向主题发送消息，所有订阅这个主题的订阅者都将收到这个消息。</td></tr><tr><td>多对一</td><td>包括多个发布者和一个订阅者。多个发布者都向某个特定的主题发送消息，订阅者会从这个主题收到消息。</td></tr><tr><td>多对多</td><td>包括多个发布者和多个订阅者。各个发布者都向某个主题发送消息，而各个订阅者都将收到这些消息。</td></tr></tbody></table><h2 id="Pub-Sub核心概念"><a href="#Pub-Sub核心概念" class="headerlink" title="Pub&#x2F;Sub核心概念"></a>Pub&#x2F;Sub核心概念</h2><p>在我们深入去了解Pub&#x2F;Sub的细节和实现之前，我们需要先对Pub&#x2F;Sub相关的核心概念有所了解。Pub&#x2F;Sub系统包含多个组件，下表描述了其中的一些主要的组件：</p><table><thead><tr><th>组件</th><th>描述</th></tr></thead><tbody><tr><td>发布者</td><td>发布者是一个应用或者服务，它会发出消息。</td></tr><tr><td>订阅者</td><td>订阅者是一个应用或者服务，它会接收消息。</td></tr><tr><td>主题<br/>(topic)</td><td>主题即消息的标题或信息源。发布者可以向一个主题发送消息，这些消息会被广播至订阅者。</td></tr><tr><td>消息</td><td>消息包含将在系统中被接收或传递的消息。</td></tr><tr><td>中介<br/>(broker)</td><td>中介负责指引消息在系统中的流动。它扮演着一个中间人的角色，负责在发布者和订阅者之间建立通信，并在它们之间交换信息。中介可以维护一个关于主题及其订阅者的列表，来帮助中介将从发布者收到的信息发送到正确的订阅者。</td></tr><tr><td>路由<br/>(routing)</td><td>路由指在系统中，信息从发布者流向订阅者，并依靠特定的订阅保障信息被送往正确的订阅者的过程。</td></tr></tbody></table><h2 id="Redis-Pub-Sub"><a href="#Redis-Pub-Sub" class="headerlink" title="Redis Pub&#x2F;Sub"></a>Redis Pub&#x2F;Sub</h2><p>现在我们已经了解了关于Pub&#x2F;Sub的一些抽象概念，并且知道了它是如何工作的。接下来我们需要深入到Redis针对Pub&#x2F;Sub的实现，来了解消息从发布者发布出来，到结束于订阅者这个过程中，系统是如何通信的。</p><p>Redis通过在客户端之间实现一个简单而高效的消息系统来实现Pub&#x2F;Sub。在Redis中，一个客户端可以“发布”一条消息到一个命名的通道，其他客户端则可以“订阅”这个通道来接收消息。</p><p>当一个客户端向这个通道发布了一条消息，Redis将会把这个消息发送到所有订阅了这个通道的客户端。这样，应用程序的不同组件之间就可以实时地通信并交换信息。</p><p>Redis Pub&#x2F;Sub提供了一个轻量级、高速，并且可扩展的消息传递解决方案，并且可以用于各种场景下，比如实现一个实时通知、在不同微服务之间发送消息，或在一个应用的不同组件之间通信。</p><h3 id="同步通信"><a href="#同步通信" class="headerlink" title="同步通信"></a>同步通信</h3><p>Redis Pub&#x2F;Sub是同步的。<strong>为了保证消息可以成功被传递，订阅者和发布者必须同时连接到Redis。</strong></p><p>你可以将它想象成一个收音机电台，在调频到这个频道之后，你就可以收听它的内容。然而，在收音机被关掉之后，你就没法再收听了。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-data-flow.png"></p><p>这意味着如果一个订阅者断开了连接，过了一会又连了上来，那么它将无法收到断开期间发出来的消息。也就是说，这限制了Redis Pub&#x2F;Sub只能用在允许数据可能丢失的场景下。</p><h3 id="发射后不管-Fire-Forget"><a href="#发射后不管-Fire-Forget" class="headerlink" title="发射后不管 (Fire &amp; Forget)"></a>发射后不管 (Fire &amp; Forget)</h3><p>“发射后不管”是一种信息传递模式，在这种模式下，发送方不会期望从接收方明确确认信息已经被收到。发送方只会将消息发出去，然后就继续开始做后面的事，不管消息有没有被接收方收到。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-relationship.png"></p><h3 id="仅向外播散-Fan-out-Only"><a href="#仅向外播散-Fan-out-Only" class="headerlink" title="仅向外播散 (Fan-out Only)"></a>仅向外播散 (Fan-out Only)</h3><p>Redis Pub&#x2F;Sub只管向外播散消息，也就是说，当发布者发布了一条消息后，这条消息会被广播到所有当前活动的订阅者。所有订阅者都会收到这条消息的副本，不管它们是不是对这条消息感兴趣。</p><h2 id="掀开Redis-Pub-Sub的引擎盖"><a href="#掀开Redis-Pub-Sub的引擎盖" class="headerlink" title="掀开Redis Pub&#x2F;Sub的引擎盖"></a>掀开Redis Pub&#x2F;Sub的引擎盖</h2><p>Redis最著名的功能是键值服务器。当客户端连接到Redis服务器后，它将会与服务器建立TCP连接，并开始向服务器发送命令。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-server-and-client.png"></p><p>但是Redis也是个消息服务器！一个对<code>topicA</code>感兴趣的客户端可以向Redis服务器建立一个TCP连接，发送<code>SUBSCRIBE topicA</code>命令，然后等待与<code>topicA</code>相关的新闻。“新闻机构”也可以连接到Redis服务器，发送<code>PUBLISH topicA message-data</code>，然后所有订阅了这个主题的客户端都将收到这条价值连城的消息。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-publish-and-subscribe.png"></p><p>如果放大了看Redis里面发生了什么，我们可以想象Redis会跟踪每个套接字(socket)的订阅集：</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-subscription-set.png"></p><p>咱们继续深入看看Redis是怎么做的。</p><p>最初的Pub&#x2F;Sub实现允许客户端发送三个新的命令：<code>PUBLISH</code>，<code>SUBSCRIBE</code>，和<code>UNSUBSCRIBE</code>。Redis使用了一个全局变量<code>pubsub_channels</code>来跟踪各个订阅，在其中Redis维护了一个通道名和订阅它的客户端对象的映射关系。每个客户端对象代表一个TCP连接的客户端，并通过其对应的文件描述符来跟踪。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-pubsub-channels.png"></p><p>当一个客户端发送<code>SUBSCRIBE</code>命令后，它对应的客户端对象就会被加到对应通道的客户端对象集中。<br>在发布信息时，Redis会从<code>pubsub_channel</code>中找到这个主题对应的订阅者，并针对每个客户端启动一个计划任务来向对应的套接字发送信息。</p><h3 id="处理连接断开"><a href="#处理连接断开" class="headerlink" title="处理连接断开"></a>处理连接断开</h3><p>客户端的连接可能会因为客户端断开连接，或者网线被拔掉而断开。当有连接断开时，Redis必须将对应的订阅清理掉。假设客户端A断开了连接，那么为了将这个客户端从<code>pubsub_channels</code>中清除，Redis需要遍历每个通道(“topicA”和”topicB”)，并从它们的客户端对象集中删掉它。</p><p>可想而知，遍历所有的通道的效率是非常低的。按道理，Redis只需要访问”topicA”这个通道，因为客户端A只订阅了它。为了将这个理论变成实际，Redis标记了各个客户端及其订阅的通道，并将其与<code>pubsub_channels</code>同步。这样，Redis就只需要访问这个客户端相关的通道，而不需要低效的遍历所有的通道。我们在图中可以将这些标记画成绿色的圆：</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-pubsub-channels-annotated-subscription.png"></p><h3 id="落实理论"><a href="#落实理论" class="headerlink" title="落实理论"></a>落实理论</h3><p>我们已经知道，全局变量<code>pubsub_channels</code>的数据结构基本上就是一个<code>Map&lt;ChannelName, Set&lt;Client&gt;&gt;</code>，而每个客户端的订阅集就是个<code>Set&lt;ChannelName&gt;</code>。但是这些都只是一个抽象的数据结构，没法说明内存中是如何表现的。所以我们继续放大，看看内存中的样子。</p><p><code>pubsub_channels</code>这个Map实际上是个哈希表，其中通道名会被哈希，然后放到一个<code>2^n</code>长的数组中的对应位置。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-pubsub-channels-hash-chain.png"></p><p>包含了8个桶的数组<code>pubsub_channels</code>是一块独立分配出来的内存空间。要发布消息到一个通道，首先它会将通道名进行哈希，找到它对应的桶，然后遍历这个通道相关联的各个客户端。然而不同的通道名又可能会被哈希到同一个桶中，为了解决这个哈希冲突问题，Redis使用了一个叫“哈希链”的方案，即每个桶都会指向一个保存着通道的链表。比如在上面的例子中，“topicA”和“topicB”都被哈希到了第三个桶中。实际上为了以防万一，Redis会在启动时为它的哈希函数选择一个随机的种子来尝试避免出现哈希碰撞，这也可以防止被恶意用户蓄意订阅到会被哈希到同一个位置的大量通道而导致性能下降。</p><p>在下图中，绿色的字符串是通道的名字，作为哈希表的键；紫色的是客户端的集合，作为哈希表的值。但“集合”在这里也是个抽象的概念，它在Redis中是怎样表现的呢？答案是，另一个链表！</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-client-set.png"></p><p>我们可能会觉得字符串“topicA”和“topicB”是在哈希链里面的，但实际不是。每个字符串都是独立分配的一块内存空间。字符串在Redis中被广泛使用着，它们甚至有自己的名字叫“简单动态字符串（Simple Dynamic Strings）”，其内容分三部分：已占用的空间、剩余的空间，和一个字符数组。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-simple-dynamic-strings.png"></p><p>到现在我们已经快要到内存级别了，但是还有一件事没有提到，那就是每个通道的客户端集。在这里，Redis并没有选择用链表，而是用了另一个哈希表，其中通道的名字就是哈希表的键。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-clients-set-of-channels.png"></p><p>我暂时还不清楚为什么Redis选择用链表保存通道的客户端集，而又选择哈希表来保存客户端的通道集。我怀疑Redis选择在通道的客户端集上用链表是因为，链表更适用于发布的场景，因为在发布时会需要遍历它；而选择在客户端的通道集上用哈希表则是因为它更适用于订阅和取消订阅的场景，因为此时要做的是在里面做查找。如果你有更深入的理解，请告诉我。</p><p>需要注意的是，在每个客户端的哈希链上，指向它的值的指针是被忽略的，这片内存没有被用到。当用哈希表来表示一个集合的时候，我们只会用到它的键。相比较于我们从代码复用中得到的收益，这部分浪费不值一提。</p><p>终于，我们快要接近真相了：图中的每个方块都代表redis-server进程中分配的一块内存。现在我们来回顾一下<code>PUBLISH</code>和<code>SUBSCRIBE</code>的算法：</p><ul><li>在<code>PUBLISH</code>的时候，首先对通道的名字做一次哈希，接下来遍历哈希链，将要发布的通道名与哈希链中的各个通道名做比较。在找到我们要的那个通道后，从中得到它的客户端列表。然后遍历客户端列表，将消息发至各个客户端。</li><li>在<code>SUBSCRIBE</code>的时候，首先像<code>PUBLISH</code>一样找到保存着客户端的那个链表，然后将新的客户端附加在链表的尾部。并且将要订阅的通道添加到客户端维护的哈希表中。</li></ul><h3 id="可以实时操作的哈希表"><a href="#可以实时操作的哈希表" class="headerlink" title="可以实时操作的哈希表"></a>可以实时操作的哈希表</h3><p>要注意这些哈希表的大小是不同的，而且都大概与其元素数量成比例。Redis会根据元素数量调整哈希表的大小。但是，Redis的设计目标是低延迟，而调整哈希表又是个比较耗时的操作。那么Redis是怎么做到在调整哈希表大小时不产生延迟尖峰的？</p><p>答案是：Redis会逐步的调整哈希表的大小。它在底层维护了新旧两个哈希表。假设<code>pubsub_channels</code>这个哈希表正在经历调整。</p><p>每当Redis对哈希表进行操作时（查询、插入、删除……），它都会稍微调整一下哈希表的大小。它会跟踪有多少老的桶被移动到了新的桶中，然后在每次操作中，它都会再移动几个过去。这样一来，调整大小的工作量就会被限制，使得Redis在调整期间也可以保证响应。</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-hashtable-resizing.png"></p><h3 id="不再订阅昂贵的东西（Expensive-unsubscribed）"><a href="#不再订阅昂贵的东西（Expensive-unsubscribed）" class="headerlink" title="不再订阅昂贵的东西（Expensive unsubscribed）"></a>不再订阅昂贵的东西（Expensive unsubscribed）</h3><p>在Pub&#x2F;Sub中还有一个很重要的操作：<code>UNSUBSCRIBE</code>，它所做的与<code>SUBSCRIBE</code>正相反，它会使客户端不再订阅到指定的通道，也不会再收到来自这个通道的消息。那么利用上面说过的数据结构，你会怎么实现<code>UNSUBSCRIBE</code>功能呢？Redis是这样做的：</p><p>在<code>UNSUBSCRIBE</code>时，首先找到这个通道对应的客户端链表，然后遍历整个链表直到找到我们要删除的那个客户端。</p><p>也就是说，<code>UNSUBSCRIBE</code>操作的耗时是<code>O(n)</code>，其中<code>n</code>就是客户端的数量。如果一个通道有大量的客户端订阅，那么<code>UNSUBSCRIBE</code>操作就会变得很昂贵。<strong>所以，你要么需要限制你的客户端数量，要么需要限制它们订阅的通道的数量。</strong><span class="exturl" data-url="aHR0cHM6Ly9wdXNoZXIuY29tLw==">Pusher<i class="fa fa-external-link-alt"></i></span>的一个优化就是去掉重复的订阅，数百万的Pusher订阅会被折叠为数量更少的Redis订阅。</p><p>Redis可以通过把存放客户端订阅的链表换成哈希表来优化，但也可能不会很理想，会导致发布消息稍稍变慢，因为遍历哈希表比遍历链表慢。Redis选择针对<code>PUBLISH</code>操作去做优化，因为它们比变更订阅更加常见。</p><h3 id="按照表达式匹配订阅（Pattern-subscriptions）"><a href="#按照表达式匹配订阅（Pattern-subscriptions）" class="headerlink" title="按照表达式匹配订阅（Pattern subscriptions）"></a>按照表达式匹配订阅（Pattern subscriptions）</h3><p>最初的Pub&#x2F;Sub提供了<code>PUBLISH</code>、<code>SUBSCRIBE</code>和<code>UNSUBSCRIBE</code>三个命令。不久之后，Redis引入了一个名为“按照表达式匹配订阅”的新功能及其命令<code>PSUBSCRIBE</code>，这个可以让客户端订阅所有匹配了某个正则表达式的主题。</p><p>现在，如果一个客户端发送了命令<code>PSUBSCRIBE food.donuts.*</code>，然后一个发布者发送了命令<code>PUBLISH food.donuts.glazed 2-for-£2</code>，那么订阅者也会收到消息，因为<code>food.donuts.glazed</code>匹配了正则表达式<code>food.donuts.*</code>。</p><p>按照表达式订阅的实现是与普通的订阅完全不同的。除了全局的<code>pubsub_channels</code>哈希表之外，还有一个全局的<code>pubsub_patterns</code>列表。这是一个包含了<code>pubsubPattern</code>对象的列表，每个对象与一个客户端订阅的一个表达式相关联。类似地，每个客户端对象也有一个包含着它订阅的表达式的链表。下图展示了当客户端B订阅了<code>drink?</code>，同时客户端A和客户端B订阅了<code>food.*</code>之后，redis-server进程的内存的样子：</p><p><img data-src="https://blog-static.boris1993.com/translation-redis-pubsub-in-depth/redis-pub-sub-pattern-subscriptions.png"></p><p>图示的左侧是一个全局的链表，每个节点都指向一个<code>pubsubPattern</code>，每个表达式在内存中都是字符串字面量。在图示的右侧，每个客户端都有一个关联着它的订阅的链表。</p><p>当一个客户端发送命令<code>PUBLISH food.donuts 5-for-£1</code>，Redis就会遍历<code>pubsub_patterns</code>列表，并将字符串<code>food.donuts</code>与每个表达式匹配。每遇到一个符合的匹配，Redis就会发送消息<code>5-for-£1</code>到关联的客户端。</p><p>这个实现可能有一个让你惊讶的点：如果多个客户端订阅到同一个表达式时，这些订阅不会被组合在一起。如果有10000个客户端都订阅了<code>food.*</code>，那么你将会得到一个包含了10000个表达式的链表，其中每个元素在发布消息的时候都会被匹配。这种设计假定模式订阅集很小，而且各不相同。</p><p>另一个会让你惊讶的点是，表达式都是以其原文存储的，它们并没有被编译。这一点尤其有趣，因为Redis的匹配函数<code>stringmatch</code>有一些有趣的坏情况。如下展示了Redis是如何把字符串<code>aa</code>与表达式<code>*a*a*b</code>相匹配的：</p><figure class="highlight plaintext"><table><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">stringmatch(&quot;*a*a*b&quot;, &quot;aa&quot;)</span><br><span class="line">    stringmatch(&quot;a*a*b&quot;, &quot;aa&quot;)</span><br><span class="line">        stringmatch(&quot;*a*b&quot;, &quot;a&quot;)</span><br><span class="line">            stringmatch(&quot;a*b&quot;, &quot;a&quot;)</span><br><span class="line">                stringmatch(&quot;*b&quot;, &quot;&quot;)</span><br><span class="line">                    stringmatch(&quot;b&quot;, &quot;&quot;)</span><br><span class="line">                        false</span><br><span class="line">            stringmatch(&quot;a*b&quot;, &quot;&quot;)</span><br><span class="line">                false</span><br><span class="line">    stringmatch(&quot;a*a*b&quot;, &quot;a&quot;)</span><br><span class="line">        stringmatch(&quot;*a*b&quot;, &quot;&quot;)</span><br><span class="line">            stringmatch(&quot;a*b&quot;, &quot;&quot;)</span><br><span class="line">                false</span><br><span class="line">    stringmatch(&quot;a*a*b&quot;, &quot;&quot;)</span><br><span class="line">        false</span><br></pre></td></tr></table></figure><p>这种带有许多<code>glob</code>的恶意表达式会导致匹配的执行时间爆炸性增长。Redis的表达式语言虽然可以被编译成确定有限状态自动机（DFA），来让匹配耗时变得线性，但却没这么做。</p><p>所以长话短说，你不应该把Redis的按表达式匹配功能开放给不被信任的客户端，因为这可能会引来两种攻击：一种是订阅大量的表达式，另一种就是刻意制造的表达式。在Pusher，我们会非常小心地处理Redis的按表达式订阅功能。</p><h2 id="Pub-Sub的使用场景"><a href="#Pub-Sub的使用场景" class="headerlink" title="Pub&#x2F;Sub的使用场景"></a>Pub&#x2F;Sub的使用场景</h2><p>我们现在知道了Pub&#x2F;Sub的技术细节，以及在Redis中是怎么实现的。接下来我们看看有哪些使用场景。</p><p>Redis提供的异步集成功能提高了系统的整体灵活性和稳定性，使其能够满足各种场景，包括：</p><ul><li>实时消息和聊天</li><li>物联网设备</li><li>新闻发布和警告</li><li>分布式计算和微服务</li><li>事件驱动的架构</li><li>组件间解耦，以及减少依赖</li><li>扇入处理（Fan-in processing）—— 将多个消息组合成一个消息的过程被称为扇入处理。</li><li>刷新分布式缓存</li></ul><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>Redis是实现Pub&#x2F;Sub最常用的工具之一。它因扩展性、低延迟和易于集成而广为人知。我们也在内存块级别探讨了Redis的工作原理。</p><p>Redis Pub&#x2F;Sub是一个高效的分发消息的方法，但我们也要知道它在哪些方面做了优化，以及在哪有陷阱。要想彻底了解的话，那就去研究源码吧！一句话，记得只在可信的环境中使用Redis，并要限制客户端的数量，并且小心处理按照表达式的订阅。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>在同一个类的两个方法内部互相调用中，如何使AOP生效</title>
      <link>https://www.boris1993.com/spring-how-to-make-aop-effective-in-internal-calls.html</link>
      <description>
        <![CDATA[<p>熟悉Spring AOP的都知道，如果同一个类中的两个方法在内部互相调用，那么此时AOP是不会生效的，因为Spring AOP是通过代理类来实现的，而类内部的方法调用并不会走到代理对象。那么，有没有办法让内部调用的时候也让AOP生效呢？万能的ChatGPT告诉我，方法是有的，还有好几种。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/Spring/">Spring</category>
      <category domain="https://www.boris1993.com/tags/AOP/">AOP</category>
      <pubDate>Mon, 10 Mar 2025 06:33:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>熟悉Spring AOP的都知道，如果同一个类中的两个方法在内部互相调用，那么此时AOP是不会生效的，因为Spring AOP是通过代理类来实现的，而类内部的方法调用并不会走到代理对象。那么，有没有办法让内部调用的时候也让AOP生效呢？万能的ChatGPT告诉我，方法是有的，还有好几种。</p><span id="more"></span><h2 id="使用-Autowired通过代理调用"><a href="#使用-Autowired通过代理调用" class="headerlink" title="使用@Autowired通过代理调用"></a>使用<code>@Autowired</code>通过代理调用</h2><p>这个方法的思路是，利用<code>@Autowired</code>注解注入自身的代理对象，然后通过代理对象完成方法调用。</p><figure class="highlight java"><table><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="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DemoService</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> DemoService demoService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">helloWorld</span><span class="params">()</span> &#123;</span><br><span class="line">        demoService.test();</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Test&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个操作我也在本地的一个Spring Boot项目中验证确实是可行的，前提是要在<code>application.properties</code>里面配置允许循环引用<code>spring.main.allow-circular-references=true</code>。而且这个操作有一个缺点，就是自身代理对象<code>demoService</code>必须通过字段注入的方式完成依赖注入，如果用构造方法注入，启动的时候就会报循环依赖错误导致项目无法成功启动（不用想也知道，在构造方法里面依赖自己肯定不行啊）。</p><p>我个人并不喜欢这个方法，一个原因是因为我不喜欢循环引用，另一个原因是我不喜欢字段注入，毕竟Spring早就推荐改成构造方法注入了。所以，我们继续看下一个方法。</p><h2 id="使用AopContext-currentProxy"><a href="#使用AopContext-currentProxy" class="headerlink" title="使用AopContext.currentProxy()"></a>使用<code>AopContext.currentProxy()</code></h2><p>另一个方法是使用<code>AopContext#currentProxy()</code>静态方法获取到当前的代理对象，也就是对象自己，然后再通过这个代理对象进行方法调用。</p><figure class="highlight java"><table><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="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DemoService</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">helloWorld</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">DemoService</span> <span class="variable">demoService</span> <span class="operator">=</span> (DemoService) AopContext.currentProxy();</span><br><span class="line">        demoService.test();</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Test&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是如果就这样运行，你就会得到这样一条错误信息：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java.lang.IllegalStateException: Cannot find current proxy: Set &#x27;exposeProxy&#x27; property on Advised to &#x27;true&#x27; to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.</span><br></pre></td></tr></table></figure><p>看来ChatGPT没把话说全啊，好在我的IDEA里面装了通义灵码，把代码上下文和这个异常抛给它之后，它告诉我要通过注解<code>@EnableAspectJAutoProxy(exposeProxy = true)</code>配置Spring AOP允许暴露当前代理对象。那么按照它的说法，我给切面配置类加上这个注解：</p><figure class="highlight java"><table><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">@Aspect</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@EnableAspectJAutoProxy(exposeProxy = true)</span> <span class="comment">// &lt;-- 就这个，它的默认值是false</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DemoAopAdvice</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Logger</span> <span class="variable">LOGGER</span> <span class="operator">=</span> LoggerFactory.getLogger(DemoAopAdvice.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Pointcut(&quot;execution(* com.example.demo.DemoService.*(..))&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Before(&quot;test()&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">before</span><span class="params">()</span> &#123;</span><br><span class="line">        LOGGER.info(<span class="string">&quot;Before&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再重启应用，就发现<code>demoService</code>里面的内部调用成功触发了AOP切面。</p><p>通义灵码还提醒我，在多线程环境中，要确保<code>AopContext#currentProxy()</code>必须在与AOP调用相同的线程中调用。此外，根据<code>currentProxy()</code>的JavaDoc，调用它的方法也必须经过了AOP调用，否则会抛出<code>IllegalStateException</code>异常。点进<code>currentProxy()</code>方法的实现，发现它内部是用ThreadLocal来保存代理对象的，同时在这个类中还有一个<code>setCurrentProxy(Object)</code>方法来把当前的代理对象保存到ThreadLocal中。下断点调试后发现，<code>setCurrentProxy(Object)</code>这个方法会先被执行，然后再走到我们实际调用的方法。这正好解释了为什么要注意在相同的线程中调用<code>AopContext#currentProxy()</code>，并且调用它的方法必须是经过了AOP调用的，因为不这样的话ThreadLocal中根本就没东西可拿。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>在Java中为什么不推荐在代码中直接写字符串值</title>
      <link>https://www.boris1993.com/java-why-prefer-string-constants-over-literals.html</link>
      <description>
        <![CDATA[<p>前两天看到个问题，说在Java中，为什么不推荐在代码中直接写字符串值？我一开始能想到的就是代码的可维护性和可读性，但是总觉得这个问题没这么简单，肯定还有JVM虚拟机里面的优化。那既然现在大语言模型这么发达，我觉得可以让它给我讲讲。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/Java/">Java</category>
      <pubDate>Thu, 06 Mar 2025 05:26:23 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>前两天看到个问题，说在Java中，为什么不推荐在代码中直接写字符串值？我一开始能想到的就是代码的可维护性和可读性，但是总觉得这个问题没这么简单，肯定还有JVM虚拟机里面的优化。那既然现在大语言模型这么发达，我觉得可以让它给我讲讲。</p><span id="more"></span><h2 id="ChatGPT的回答"><a href="#ChatGPT的回答" class="headerlink" title="ChatGPT的回答"></a>ChatGPT的回答</h2><blockquote><p>在 Java 代码中直接写字符串值（即 硬编码字符串）通常是不推荐的，主要从以下两个方面考虑：</p><ol><li>代码可读性和可维护性</li></ol><ul><li>降低可读性：直接在代码中写死字符串值，容易让代码的意图变得不清晰，阅读时难以理解这个字符串的作用。</li><li>难以维护：如果字符串值在多个地方使用，一旦需要修改，就必须逐个查找替换，容易遗漏或出错。</li><li>不利于国际化（i18n）：如果程序需要支持多种语言，硬编码字符串必须手动提取，而如果使用常量或资源文件，则可以更方便地进行国际化处理。</li></ul><ol start="2"><li>JVM 内部优化</li></ol><ul><li><strong>字符串常量池优化</strong>：JVM 会对字符串字面量进行**字符串常量池（String Pool）**处理，避免重复创建相同的字符串实例。但如果字符串值是动态拼接或new关键字创建的，就可能无法享受这种优化。</li><li><strong>减少堆内存占用</strong>：使用<code>String</code>变量或<code>final</code>常量，JVM 可能会进行<code>常量折叠</code>，减少运行时重复创建的开销，提高效率。</li><li><strong>提高性能</strong>：直接写字符串字面量可能会在某些情况下导致 JVM 额外分配对象，而使用<code>final</code>常量或<code>intern()</code>方法可以优化内存占用。</li></ul></blockquote><p>果然除了可读性考虑，JVM也会对字符串常量做一些优化。但，什么是<code>常量折叠</code>？</p><h2 id="常量折叠"><a href="#常量折叠" class="headerlink" title="常量折叠"></a>常量折叠</h2><p>追问ChatGPT什么是常量折叠后，它是这么回到我的：</p><blockquote><p>常量折叠（Constant Folding） 是 JVM 编译优化（Just-In-Time Compilation, JIT） 或 Java 编译器（javac）在编译阶段执行的一种优化策略，它会在 编译期 计算出可以确定的常量表达式，并用计算结果替换原始表达式，从而减少运行时的计算开销。</p><ol><li>常量折叠的作用</li></ol><p>常量折叠的核心目标是提前计算出能确定的值，避免运行时重复计算，从而提高性能。例如，以下代码：</p><figure class="highlight java"><table><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_">Test</span> &#123;</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> &#123;</span><br><span class="line">       <span class="type">int</span> <span class="variable">a</span> <span class="operator">=</span> <span class="number">2</span> * <span class="number">3</span>;  <span class="comment">// 计算常量表达式</span></span><br><span class="line">       System.out.println(a);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在编译阶段，JVM 会将<code>2 * 3</code>计算为<code>6</code>，然后直接将<code>6</code>存入字节码，而不是在运行时执行乘法计算。</p><ol start="2"><li>常量折叠的典型场景</li></ol><p>（前略）</p><p>（2）字符串常量优化<br>如果字符串是常量，编译器会在编译时进行字符串拼接优化，避免运行时的<code>StringBuilder</code>操作：</p><figure class="highlight java"><table><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">StringConstantFolding</span> &#123;</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> &#123;</span><br><span class="line">       <span class="keyword">final</span> <span class="type">String</span> <span class="variable">s1</span> <span class="operator">=</span> <span class="string">&quot;Hello, &quot;</span>;</span><br><span class="line">       <span class="keyword">final</span> <span class="type">String</span> <span class="variable">s2</span> <span class="operator">=</span> <span class="string">&quot;World!&quot;</span>;</span><br><span class="line">       <span class="type">String</span> <span class="variable">s3</span> <span class="operator">=</span> s1 + s2;  <span class="comment">// 直接折叠为 &quot;Hello, World!&quot;</span></span><br><span class="line">       System.out.println(s3);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>优化后的字节码相当于：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">s3</span> <span class="operator">=</span> <span class="string">&quot;Hello, World!&quot;</span>;</span><br></pre></td></tr></table></figure><p>（后略）</p><ol start="3"><li>什么时候不会发生常量折叠？<br>（1）变量不是<code>final</code> - 如果变量不是<code>final</code>，编译器无法确定其值，因此不会进行常量折叠<br>（2）值在运行时才能确定 - 如果值依赖于外部输入、方法调用、对象实例等，JVM 不能在编译时优化</li></ol></blockquote><p>也就是说，如果在代码里这样写：</p><figure class="highlight java"><table><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</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">HELLO</span> <span class="operator">=</span> <span class="string">&quot;hello, &quot;</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">WORLD</span> <span class="operator">=</span> <span class="string">&quot;world&quot;</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> &#123;</span><br><span class="line">        System.out.println(HELLO + WORLD);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么实际上JVM就会给你优化成：</p><figure class="highlight java"><table><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span>&#123;</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">HELLO</span> <span class="operator">=</span> <span class="string">&quot;hello, &quot;</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">WORLD</span> <span class="operator">=</span> <span class="string">&quot;world&quot;</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> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;hello, world&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将这段代码编译后再用<code>javap</code>反编译，可以看到这样的结果：</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">Classfile /Users/boris1993/Developer/java/leetcode/src/Test.class</span><br><span class="line">  Last modified Mar 6, 2025; size 523 bytes</span><br><span class="line">  SHA-256 checksum 3443c8e0a13cd862c0195b772bb4290bde7e1d07b50818d531c2b5d66d665270</span><br><span class="line">  Compiled from &quot;Test.java&quot;</span><br><span class="line">public class Test</span><br><span class="line">  minor version: 0</span><br><span class="line">  major version: 65</span><br><span class="line">  flags: (0x0021) ACC_PUBLIC, ACC_SUPER</span><br><span class="line">  this_class: #13                         // Test</span><br><span class="line">  super_class: #2                         // java/lang/Object</span><br><span class="line">  interfaces: 0, fields: 2, methods: 2, attributes: 1</span><br><span class="line">Constant pool:</span><br><span class="line">   #1 = Methodref          #2.#3          // java/lang/Object.&quot;&lt;init&gt;&quot;:()V</span><br><span class="line">   #2 = Class              #4             // java/lang/Object</span><br><span class="line">   #3 = NameAndType        #5:#6          // &quot;&lt;init&gt;&quot;:()V</span><br><span class="line">   #4 = Utf8               java/lang/Object</span><br><span class="line">   #5 = Utf8               &lt;init&gt;</span><br><span class="line">   #6 = Utf8               ()V</span><br><span class="line">   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;</span><br><span class="line">   #8 = Class              #10            // java/lang/System</span><br><span class="line">   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;</span><br><span class="line">  #10 = Utf8               java/lang/System</span><br><span class="line">  #11 = Utf8               out</span><br><span class="line">  #12 = Utf8               Ljava/io/PrintStream;</span><br><span class="line">  #13 = Class              #14            // Test</span><br><span class="line">  #14 = Utf8               Test</span><br><span class="line">  #15 = String             #16            // hello, world</span><br><span class="line">  #16 = Utf8               hello, world</span><br><span class="line">  #17 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V</span><br><span class="line">  #18 = Class              #20            // java/io/PrintStream</span><br><span class="line">  #19 = NameAndType        #21:#22        // println:(Ljava/lang/String;)V</span><br><span class="line">  #20 = Utf8               java/io/PrintStream</span><br><span class="line">  #21 = Utf8               println</span><br><span class="line">  #22 = Utf8               (Ljava/lang/String;)V</span><br><span class="line">  #23 = Utf8               HELLO</span><br><span class="line">  #24 = Utf8               Ljava/lang/String;</span><br><span class="line">  #25 = Utf8               ConstantValue</span><br><span class="line">  #26 = String             #27            // hello,</span><br><span class="line">  #27 = Utf8               hello,</span><br><span class="line">  #28 = Utf8               WORLD</span><br><span class="line">  #29 = String             #30            // world</span><br><span class="line">  #30 = Utf8               world</span><br><span class="line">  #31 = Utf8               Code</span><br><span class="line">  #32 = Utf8               LineNumberTable</span><br><span class="line">  #33 = Utf8               main</span><br><span class="line">  #34 = Utf8               ([Ljava/lang/String;)V</span><br><span class="line">  #35 = Utf8               SourceFile</span><br><span class="line">  #36 = Utf8               Test.java</span><br><span class="line">&#123;</span><br><span class="line">  public Test();</span><br><span class="line">    descriptor: ()V</span><br><span class="line">    flags: (0x0001) ACC_PUBLIC</span><br><span class="line">    Code:</span><br><span class="line">      stack=1, locals=1, args_size=1</span><br><span class="line">         0: aload_0</span><br><span class="line">         1: invokespecial #1                  // Method java/lang/Object.&quot;&lt;init&gt;&quot;:()V</span><br><span class="line">         4: return</span><br><span class="line">      LineNumberTable:</span><br><span class="line">        line 1: 0</span><br><span class="line"></span><br><span class="line">  public static void main(java.lang.String[]);</span><br><span class="line">    descriptor: ([Ljava/lang/String;)V</span><br><span class="line">    flags: (0x0009) ACC_PUBLIC, ACC_STATIC</span><br><span class="line">    Code:</span><br><span class="line">      stack=2, locals=1, args_size=1</span><br><span class="line">         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;</span><br><span class="line">         3: ldc           #15                 // String hello, world</span><br><span class="line">         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V</span><br><span class="line">         8: return</span><br><span class="line">      LineNumberTable:</span><br><span class="line">        line 6: 0</span><br><span class="line">        line 7: 8</span><br><span class="line">&#125;</span><br><span class="line">SourceFile: &quot;Test.java&quot;</span><br></pre></td></tr></table></figure><p>首先可以看到，在常量池中有这么两行代码：</p><figure class="highlight plaintext"><table><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">#15 = String             #16            // hello, world</span><br><span class="line">#16 = Utf8               hello, world</span><br></pre></td></tr></table></figure><p>证明代码中<code>HELLO + WORLD</code>的值已经被编译器优化成了一个计算好的常量。在<code>main</code>方法的字节码中也可以看到</p><figure class="highlight plaintext"><table><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">public static void main(java.lang.String[]);</span><br><span class="line">  descriptor: ([Ljava/lang/String;)V</span><br><span class="line">  flags: (0x0009) ACC_PUBLIC, ACC_STATIC</span><br><span class="line">  Code:</span><br><span class="line">    stack=2, locals=1, args_size=1</span><br><span class="line">       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;</span><br><span class="line">       3: ldc           #15                 // String hello, world</span><br><span class="line">       5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V</span><br><span class="line">       8: return</span><br></pre></td></tr></table></figure><p>JVM直接加载了”hello, world”这个字符串，而不是调用了<code>InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;</code>在运行时进行字符串拼接。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>综上所述，使用字符串常量而不是直接写字符串值，除了让代码更可读更易于维护之外，我感觉最主要的优点就是针对字符串常量的拼接结果也会被放到常量池中，避免了在运行时反复创建新的字符串对象，造成性能浪费。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>在Dev Container里写Hexo博客</title>
      <link>https://www.boris1993.com/hexo-in-dev-container.html</link>
      <description>
        <![CDATA[<p>我们都知道，Docker和容器化技术让运维有了质的飞跃，从此我们不必再担心软件运行所需的繁杂环境，只需要拉取镜像并运行就可以一步到位部署好软件的运行环境。但是在开发过程中，我们仍然需要在本机安装各种依赖，一不小心又会把本机的环境搞乱掉，更不提不同版本的语言之间可能存在的冲突。因此，就出现了Dev Container，顾名思义就是在容器环境中开发，这样我们在开发时也可以享受到统一且隔离的开发环境。</p>
<p>正好我最近也在开始接触Dev Container并使用它给一个开源项目成功提交了代码，正好趁热打铁，把写博客的Hexo环境从本机挪到Dev Container中。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/Hexo/">Hexo</category>
      <category domain="https://www.boris1993.com/tags/Dev-Container/">Dev Container</category>
      <pubDate>Wed, 25 Dec 2024 08:40:19 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>我们都知道，Docker和容器化技术让运维有了质的飞跃，从此我们不必再担心软件运行所需的繁杂环境，只需要拉取镜像并运行就可以一步到位部署好软件的运行环境。但是在开发过程中，我们仍然需要在本机安装各种依赖，一不小心又会把本机的环境搞乱掉，更不提不同版本的语言之间可能存在的冲突。因此，就出现了Dev Container，顾名思义就是在容器环境中开发，这样我们在开发时也可以享受到统一且隔离的开发环境。</p><p>正好我最近也在开始接触Dev Container并使用它给一个开源项目成功提交了代码，正好趁热打铁，把写博客的Hexo环境从本机挪到Dev Container中。</p><span id="more"></span><h2 id="配置VSCode"><a href="#配置VSCode" class="headerlink" title="配置VSCode"></a>配置VSCode</h2><p>我的计划是用VSCode来在容器中写博客，那么为了让VSCode支持Dev Container，我们需要安装<code>Dev Containers</code>这个VSCode扩展。哦对，你还得有个Docker，不论是本机还是在哪个服务器上。我这就用本机的Docker演示了。</p><p>装好扩展之后，就可以开始写Dev Container的配置了。因为不同项目对应的开发容器必然是不同的，所以Dev Container的配置会放到项目目录中。通常来说，Dev Container相关的文件都会放到项目根目录下的<code>.devcontainer</code>目录中，所以我们可以在项目目录下创建<code>.devcontainer/devcontainer.json</code>，并填入如下内容：</p><figure class="highlight json"><table><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">// Dev Container的名字</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;blog&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// 基础Docker镜像</span></span><br><span class="line">    <span class="attr">&quot;image&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node:lts-alpine&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// 容器创建好之后要执行的命令</span></span><br><span class="line">    <span class="attr">&quot;postCreateCommand&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sh .devcontainer/post_create.sh&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">// 要转发到主机的端口</span></span><br><span class="line">    <span class="attr">&quot;forwardPorts&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="number">4000</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>因为基础镜像<code>node:lts-alpine</code>中并不包含我们需要的<code>git</code>、<code>gpg</code>、<code>hexo</code>等环境，所以我们需要通过<code>postCreateCommand</code>指定容器在启动之后执行命令来安装这些依赖。<code>post_create.sh</code>的内容如下：</p><figure class="highlight bash"><table><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="meta">#!/bin/sh</span></span><br><span class="line"></span><br><span class="line">apk add --no-cache gpg git</span><br><span class="line">npm install -g hexo-cli</span><br></pre></td></tr></table></figure><p>然后用快捷键<code>ctrl-shift-p</code>（macOS就是<code>⌘-shift-p</code>）唤出命令面板，选择<code>Dev Containers: Rebuild and Reopen in Container</code>来构建容器并进入容器开发。如果将来<code>devcontainer.json</code>或相关的配置发生变化，我们也可以在命令面板中选择<code>Dev Containers: Rebuild Container</code>来重建容器。</p><h2 id="编写容器的Dockerfile"><a href="#编写容器的Dockerfile" class="headerlink" title="编写容器的Dockerfile"></a>编写容器的Dockerfile</h2><p>上面提到的通过<code>postCreateCommand</code>来安装依赖的方式，虽然可行，但是不优雅，毕竟每次创建容器都要重新安装一次，就很浪费。所以我们也可以给我们的Dev Container写一个Dockerfile，这样就可以只初始化一次然后一直用下去了。</p><figure class="highlight dockerfile"><table><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="keyword">FROM</span> node:lts-alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">LABEL</span><span class="language-bash"> devcontainer.metadata = <span class="string">&#x27;[&#123; \</span></span></span><br><span class="line"><span class="string"><span class="language-bash">    &quot;forwardPorts&quot;: [4000] \</span></span></span><br><span class="line"><span class="string"><span class="language-bash">&#125;]&#x27;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apk add --no-cache gpg git \</span></span><br><span class="line"><span class="language-bash"> &amp;&amp; npm install -g hexo-cli</span></span><br></pre></td></tr></table></figure><p>与部署用的Dockerfile不同，我们只需要写配置环境相关的命令就可以，不需要把工作空间拷贝进去，因为Dev Container会自动把这个目录mount到容器中。此外，在Dockerfile中还可以在<code>devcontainer.metadata</code>中提前指定好一些配置，以减少<code>devcontainer.json</code>的行数。</p><h2 id="配置GPG"><a href="#配置GPG" class="headerlink" title="配置GPG"></a>配置GPG</h2><p>上面一步完成之后，Dev Container的配置其实就完成了。但如果你像我一样为Git配置了GPG签名，同时又是在macOS下开发，那么你还需要对GPG做一些额外的配置。首先我们需要安装<code>pinentry-mac</code>，然后编辑<code>~/.gnupg/gpg-agent.conf</code>文件，添加一行<code>pinentry-program /usr/local/bin/pinentry-mac</code>。如果你曾经在JetBrains IDE中点过它的<code>Configure GPG Agent to support own pinentry</code>，那么你就要把它添加的那行<code>pinentry-program</code>替换成这一条，否则我们在容器中提交更新的时候，GPG会因为找不到<code>/Users/username/.gnupg/pinentry-ide.sh</code>而报错。</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><span class="exturl" data-url="aHR0cHM6Ly9jb2RlLnZpc3VhbHN0dWRpby5jb20vZG9jcy9kZXZjb250YWluZXJzL2NvbnRhaW5lcnM=">Developing inside a Container<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9jb2RlLnZpc3VhbHN0dWRpby5jb20vcmVtb3RlL2FkdmFuY2VkY29udGFpbmVycy9kZXZlbG9wLXJlbW90ZS1ob3N0">Develop on a remote Docker host<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9jbGF3cy50b3AvMjAyMi8xMi8wNi8lRTQlQkQlQkYlRTclOTQlQThEZXYtQ29udGFpbmVyJUU1JUJDJTgwJUU1JThGJTkxLw==">使用Dev Container开发 - Claws小花园<i class="fa fa-external-link-alt"></i></span></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>记给Ubuntu的LVM卷扩容</title>
      <link>https://www.boris1993.com/expanding-ubuntu-lvm-partition.html</link>
      <description>
        <![CDATA[<p>刚刚看到我虚拟机里面的Ubuntu报硬盘空间不足，看了下发现是在安装那会，自动分区就只用了一半的空间，遂着手扩容。在这里记录下操作过程备忘。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/tips/">小技巧</category>
      <category domain="https://www.boris1993.com/tags/Ubuntu/">Ubuntu</category>
      <category domain="https://www.boris1993.com/tags/LVM/">LVM</category>
      <pubDate>Mon, 09 Dec 2024 13:12:39 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>刚刚看到我虚拟机里面的Ubuntu报硬盘空间不足，看了下发现是在安装那会，自动分区就只用了一半的空间，遂着手扩容。在这里记录下操作过程备忘。</p><span id="more"></span><p>看见硬盘空间不足，首先下意识<code>df -h</code>了一下，确实满了。</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">$ df -h</span><br><span class="line">Filesystem                         Size  Used Avail Use% Mounted on</span><br><span class="line">tmpfs                              392M  3.5M  389M   1% /run</span><br><span class="line">efivarfs                            64K   41K   19K  70% /sys/firmware/efi/efivars</span><br><span class="line">/dev/mapper/ubuntu--vg-ubuntu--lv   48G   45G  314M 100% /</span><br><span class="line">tmpfs                              2.0G     0  2.0G   0% /dev/shm</span><br><span class="line">tmpfs                              5.0M     0  5.0M   0% /run/lock</span><br><span class="line">/dev/sda2                          2.0G  266M  1.6G  15% /boot</span><br><span class="line">/dev/sda1                          1.1G  6.2M  1.1G   1% /boot/efi</span><br><span class="line">tmpfs                              392M   12K  392M   1% /run/user/1000</span><br></pre></td></tr></table></figure><p>但是ESXi里面显示这个虚拟机是有一个100GB的虚拟磁盘的，而<code>df</code>只显示出将近50GB，那就是说要么有50GB没分，要么出别的问题了。首先排除最简单的情况，看看是不是有一半的硬盘没分进去。</p><figure class="highlight plaintext"><table><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">$ sudo lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL</span><br><span class="line">NAME                      FSTYPE        SIZE MOUNTPOINT                    LABEL</span><br><span class="line">loop0                                  10.1M /snap/canonical-livepatch/282 </span><br><span class="line">loop1                                  10.7M /snap/canonical-livepatch/286 </span><br><span class="line">loop2                                   104M /snap/core/16928              </span><br><span class="line">loop3                                 104.2M /snap/core/17200              </span><br><span class="line">loop4                                  55.7M /snap/core18/2829             </span><br><span class="line">loop5                                  55.4M /snap/core18/2846             </span><br><span class="line">loop6                                    64M /snap/core20/2379             </span><br><span class="line">loop7                                  63.7M /snap/core20/2434             </span><br><span class="line">loop9                                  73.9M /snap/core22/1663             </span><br><span class="line">loop10                                   71M /snap/prometheus/86           </span><br><span class="line">loop11                                 38.8M /snap/snapd/21759             </span><br><span class="line">loop12                                 44.3M /snap/snapd/23258             </span><br><span class="line">loop13                                 73.9M /snap/core22/1722             </span><br><span class="line">sda                                     100G                               </span><br><span class="line">├─sda1                    vfat            1G /boot/efi                     </span><br><span class="line">├─sda2                    ext4            2G /boot                         </span><br><span class="line">└─sda3                    LVM2_member  96.9G                               </span><br><span class="line">  └─ubuntu--vg-ubuntu--lv ext4         48.5G /                             </span><br><span class="line">sr0                                    1024M                               </span><br></pre></td></tr></table></figure><p>从上面的命令输出可以看到，<code>sda</code>这块盘的100GB都认出来了，<code>sda3</code>分区也确实分了96.9GB，但是<code>ubuntu--vg-ubuntu--lv</code>这个卷只分配了48.5GB，也就是整个LVM的一半。</p><p>查阅了教程，了解到可以用<code>lvdisplay</code>来检查LVM逻辑卷的空间，并可以用<code>vgdisplay</code>检查volume group的空间。遂分别执行。</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">$ sudo vgdisplay</span><br><span class="line">  --- Volume group ---</span><br><span class="line">  VG Name               ubuntu-vg</span><br><span class="line">  System ID             </span><br><span class="line">  Format                lvm2</span><br><span class="line">  Metadata Areas        1</span><br><span class="line">  Metadata Sequence No  2</span><br><span class="line">  VG Access             read/write</span><br><span class="line">  VG Status             resizable</span><br><span class="line">  MAX LV                0</span><br><span class="line">  Cur LV                1</span><br><span class="line">  Open LV               1</span><br><span class="line">  Max PV                0</span><br><span class="line">  Cur PV                1</span><br><span class="line">  Act PV                1</span><br><span class="line">  VG Size               &lt;96.95 GiB</span><br><span class="line">  PE Size               4.00 MiB</span><br><span class="line">  Total PE              24818</span><br><span class="line">  Alloc PE / Size       12409 / 48.47 GiB</span><br><span class="line">  Free  PE / Size       12409 / 48.47 GiB</span><br><span class="line">  VG UUID               zaKkwe-YemQ-dRcV-mrhe-v181-0Ixc-0KVaVL</span><br><span class="line"></span><br><span class="line">$ sudo pvdisplay</span><br><span class="line">  --- Physical volume ---</span><br><span class="line">  PV Name               /dev/sda3</span><br><span class="line">  VG Name               ubuntu-vg</span><br><span class="line">  PV Size               &lt;96.95 GiB / not usable 3.00 MiB</span><br><span class="line">  Allocatable           yes </span><br><span class="line">  PE Size               4.00 MiB</span><br><span class="line">  Total PE              24818</span><br><span class="line">  Free PE               12409</span><br><span class="line">  Allocated PE          12409</span><br><span class="line">  PV UUID               3FWWGz-CbIQ-BI9e-vMMd-p0Hu-eWUa-rZSeK8</span><br></pre></td></tr></table></figure><p>其实看不看都一样，已经知道根源是空间没全部分给这个卷。那么接下来就可以用<code>lvextend</code>来扩展指定的卷。我直接把所有的空余空间全都分配进去。</p><figure class="highlight plaintext"><table><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">$ sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv </span><br><span class="line">  Size of logical volume ubuntu-vg/ubuntu-lv changed from 48.47 GiB (12409 extents) to &lt;96.95 GiB (24818 extents).</span><br><span class="line">  Logical volume ubuntu-vg/ubuntu-lv successfully resized.</span><br></pre></td></tr></table></figure><p>OK，卷扩容成功。但这时候实际的文件系统大小还没变，接下来需要用<code>resize2fs</code>扩容文件系统。</p><figure class="highlight plaintext"><table><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">$ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv </span><br><span class="line">resize2fs 1.47.0 (5-Feb-2023)</span><br><span class="line">Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required</span><br><span class="line">old_desc_blocks = 7, new_desc_blocks = 13</span><br><span class="line">The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 25413632 (4k) blocks long.</span><br></pre></td></tr></table></figure><p>完事，这时候再<code>df -h</code>就能看到根挂载点有将近100GB的空间了，扩容成功。</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">$ df -h</span><br><span class="line">Filesystem                         Size  Used Avail Use% Mounted on</span><br><span class="line">tmpfs                              392M  3.5M  389M   1% /run</span><br><span class="line">efivarfs                            64K   41K   19K  70% /sys/firmware/efi/efivars</span><br><span class="line">/dev/mapper/ubuntu--vg-ubuntu--lv   96G   45G   47G  50% /</span><br><span class="line">tmpfs                              2.0G     0  2.0G   0% /dev/shm</span><br><span class="line">tmpfs                              5.0M     0  5.0M   0% /run/lock</span><br><span class="line">/dev/sda2                          2.0G  266M  1.6G  15% /boot</span><br><span class="line">/dev/sda1                          1.1G  6.2M  1.1G   1% /boot/efi</span><br><span class="line">tmpfs                              392M   12K  392M   1% /run/user/1000</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>找到并处理Docker容器中的僵尸进程</title>
      <link>https://www.boris1993.com/finding-the-docker-container-of-a-zombie-process.html</link>
      <description>
        <![CDATA[<p>刚刚登录到我的服务器之后，看到motd提示有一个僵尸进程。本来处理僵尸进程很简单，杀掉它的父进程就行了。但是紧接着我发现这个进程是属于一个Docker容器的，因为我想要更优雅地处理掉它，就顺藤摸瓜找到了对应的容器并将其重启了。这里就记录下我的排查过程以供参考。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/tips/">小技巧</category>
      <category domain="https://www.boris1993.com/tags/Docker/">Docker</category>
      <category domain="https://www.boris1993.com/tags/Zombie-Process/">Zombie Process</category>
      <category domain="https://www.boris1993.com/tags/%E5%83%B5%E5%B0%B8%E8%BF%9B%E7%A8%8B/">僵尸进程</category>
      <pubDate>Thu, 19 Sep 2024 16:14:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>刚刚登录到我的服务器之后，看到motd提示有一个僵尸进程。本来处理僵尸进程很简单，杀掉它的父进程就行了。但是紧接着我发现这个进程是属于一个Docker容器的，因为我想要更优雅地处理掉它，就顺藤摸瓜找到了对应的容器并将其重启了。这里就记录下我的排查过程以供参考。</p><span id="more"></span><p>因为僵尸进程在<code>ps</code>中的状态是<code>Z</code>，所以我首先用<code>ps aux | grep &#39;Z&#39;</code>找到这个僵尸进程的PID。此外因为僵尸进程无法被直接杀死，只能杀掉其父进程来将其连根拔起，所以我还需要用<code>pstree</code>命令找到它的父进程PID。</p><figure class="highlight console"><table><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="meta prompt_">$ </span><span class="language-bash">ps aux | grep <span class="string">&#x27;Z&#x27;</span></span></span><br><span class="line">USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND</span><br><span class="line">root      630024  0.0  0.0      0     0 ?        Z    00:55   0:00 [wget] &lt;defunct&gt;</span><br><span class="line">boris19+ 1180864  0.0  0.0   9696  2332 pts/0    S+   23:52   0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox Z</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">pstree -p -s 630024</span></span><br><span class="line">systemd(1)───containerd-shim(2642178)───node(2642199)───wget(630024)</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ps aux | grep 2642199</span></span><br><span class="line">boris19+ 1181119  0.0  0.0   9696  2288 pts/0    S+   23:53   0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox 2642199</span><br><span class="line">root     2642199  0.0  0.8 21690116 35956 ?      Ssl  Sep07   3:21 node /app/dist/index.js</span><br></pre></td></tr></table></figure><p>这里可以看到，僵尸<code>wget</code>的父进程是<code>node</code>，它的PID是<code>2642199</code>，而<code>node</code>的父进程是<code>containerd-shim</code>，也就是说这是一个Docker容器里的进程。我不确定这时候直接杀掉<code>node</code>能不能解决问题。但是老话说，来都来了，那不如继续挖下去。所以我开始尝试去找这个<code>node</code>是哪个容器运行的。</p><p>在这我稍微走了点弯路。一开始我是想通过搜索<code>index.js</code>关键词来找到容器，所以执行了下<code>docker ps | grep index.js</code>，但是显然这行不通，不然也不会有这篇文了。在稍微网上冲浪之后，我学到了一个新命令<code>systemd-cgls</code>，它可以递归展示出cgroup的内容。因为Docker用到的技术之一就是cgroup，那么想必这就是突破口。在执行了它之后，它打出来了一大片东西，就像这样：</p><figure class="highlight plaintext"><table><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></pre></td><td class="code"><pre><span class="line">Control group /:</span><br><span class="line">-.slice</span><br><span class="line">├─user.slice </span><br><span class="line">│ └─user-1000.slice </span><br><span class="line">│   ├─user@1000.service </span><br><span class="line">│   │ └─init.scope </span><br><span class="line">│   │   ├─1180199 /lib/systemd/systemd --user</span><br><span class="line">│   │   └─1180200 (sd-pam)</span><br><span class="line">│   └─session-35890.scope </span><br><span class="line">│     ├─1180196 sshd: boris1993 [priv]</span><br><span class="line">│     ├─1180374 sshd: boris1993@pts/0</span><br><span class="line">│     ├─1180376 -zsh</span><br><span class="line">│     ├─1182022 systemd-cgls</span><br><span class="line">│     └─1182023 pager</span><br><span class="line">├─init.scope </span><br><span class="line">│ └─1 /sbin/init</span><br><span class="line">└─system.slice </span><br><span class="line">  ├─irqbalance.service </span><br><span class="line">  │ └─941 /usr/sbin/irqbalance --foreground</span><br><span class="line">  ├─docker-27434de50f56b4e096bf5f38bafc76f5c74622c758be1a3f2b9531a3549f4550.scope </span><br><span class="line">  │ └─3204915 /jellyfin/jellyfin</span><br><span class="line">  ├─docker-adf03dfa49427d2d651cd9101c3033adb00396ed45c390dd6bfda0b9b73eeae3.scope </span><br><span class="line">  │ └─3775 /watchtower</span><br><span class="line">  ├─open-vm-tools.service </span><br><span class="line">  │ └─812 /usr/bin/vmtoolsd</span><br><span class="line">  ├─containerd.service </span><br><span class="line">  │ ├─   1017 /usr/bin/containerd</span><br><span class="line">  │ ├─   2197 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 0e752935d348c3a41d66fe539a06268ee29c6975d1d8f0da41bd0c52b5adf337&gt;</span><br><span class="line">  │ ├─   2479 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6f5e80ca8977314f3d3a9a47a93509438040a6908bde34cc578cdd3e8263c01a&gt;</span><br><span class="line">  │ ├─   2497 /usr/bin/containerd-shim-runc-v2 -namespace moby -id db3750e57842656c285e9641c4391df926721c94722bfb323789967c3a76d23f&gt;</span><br><span class="line">  │ ├─   2886 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 46520cd2506055c50ef361f50381a65b5077ee7b7d2607e26290d70f8b7292ba&gt;</span><br><span class="line">  │ ├─   3449 /usr/bin/containerd-shim-runc-v2 -namespace moby -id d45b7c4e806f1c0e681f317b548f795ff09eab53bc4d1a6bde05aa1fbb5064ee&gt;</span><br><span class="line">  │ ├─   3451 /usr/bin/containerd-shim-runc-v2 -namespace moby -id adf03dfa49427d2d651cd9101c3033adb00396ed45c390dd6bfda0b9b73eeae3&gt;</span><br><span class="line">  │ ├─   3554 /usr/bin/containerd-shim-runc-v2 -namespace moby -id fd2c2b6dfd77b23c0cdd59d50b33bbf70ab8046bf27ffeb595fbeadadf0a4a99&gt;</span><br><span class="line">  │ ├─   3837 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6195cb19666ad7c58ce26a115ba4f2ea3b32185acb0c33ff80cbade860693fd0&gt;</span><br><span class="line">  │ ├─   3840 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 559a99dc9580a0ddea8501df71bbce3affb90cf3a91152b8274ae508c20e0f32&gt;</span><br><span class="line">  │ ├─   3901 /usr/bin/containerd-shim-runc-v2 -namespace moby -id b11d850487921e336178dd9267b0f281eea279d0135d1d5b853e63f945246baf&gt;</span><br><span class="line">  │ ├─   3964 /usr/bin/containerd-shim-runc-v2 -namespace moby -id f3f9400aeab6331d55166b1c80cd3e27dc2083664a6126cccf279340ab738364&gt;</span><br><span class="line">  │ ├─   5852 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 722497f0a71df4060151468e590b5a69a49332ba379b17c9504077b18bbd1dfc&gt;</span><br></pre></td></tr></table></figure><p>好在它的输出就像<code>less</code>一样，可以上下卷动，也可以用<code>ed</code>命令（就像<code>vi</code>一样），那么自然而然，我可以用<code>/2642199</code>来找到那一行。</p><figure class="highlight plaintext"><table><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">├─docker-ce8f490423d791ed5f6d1aa2b705d797fc3fd2ddc34816d47c228f6fb9a20c63.scope </span><br><span class="line">│ └─2642199 node /app/dist/index.js</span><br></pre></td></tr></table></figure><p>好，现在我们知道了容器的ID是<code>ce8f490423d7</code>。虽然现在就可以重启它了，但是我的好奇心不允许我这么做，我想知道是哪个容器。所以</p><figure class="highlight plaintext"><table><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">$ docker ps | grep ce8f490423d</span><br><span class="line">ce8f490423d7   boris1993/qinglong-bot:latest                                      &quot;docker-entrypoint.s…&quot;   12 days ago     Up 12 days (healthy)   0.0.0.0:3001-&gt;3000/tcp, :::3001-&gt;3000/tcp                                                                                     qinglong-bot</span><br></pre></td></tr></table></figure><p>啊好吧，竟然是我那个青龙的机器人。这里面我只用了<code>wget</code>来做容器的liveness check，我不理解这么简单的操作怎么就能整出一个僵尸进程来。算了，以后有空再分析吧。重启容器，杀掉僵尸，睡觉。</p><figure class="highlight plaintext"><table><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">$ docker restart ce8f490423d</span><br><span class="line">ce8f490423d</span><br></pre></td></tr></table></figure><p>这时候再用<code>ps aux | grep &#39;Z&#39;</code>就看不到有僵尸进程，也就说明处置成功了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>在TypeScript的catch代码块中获取错误信息</title>
      <link>https://www.boris1993.com/translation-get-a-catch-block-error-message-with-typescript.html</link>
      <description>
        <![CDATA[<p>最近在试着用TypeScript写点东西，在用<code>catch (error) {}</code>代码块处理异常的时候，看到了一个很难理解的错误<code>TS18046: error is of type unknown</code>。网上一顿冲浪之后，看到了Kent C. Dodds的一篇博客<span class="exturl" data-url="aHR0cHM6Ly9rZW50Y2RvZGRzLmNvbS9ibG9nL2dldC1hLWNhdGNoLWJsb2NrLWVycm9yLW1lc3NhZ2Utd2l0aC10eXBlc2NyaXB0">Get a catch block error message with TypeScript<i class="fa fa-external-link-alt"></i></span>。我跟着文章的内容成功解决了这个问题，并且解答了我的疑惑，所以想要翻译出来帮助到更多的人。</p>
<p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失或功能异常）负责。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/translations/">翻译</category>
      <category domain="https://www.boris1993.com/tags/TypeScript/">TypeScript</category>
      <pubDate>Fri, 21 Jun 2024 14:53:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近在试着用TypeScript写点东西，在用<code>catch (error) {}</code>代码块处理异常的时候，看到了一个很难理解的错误<code>TS18046: error is of type unknown</code>。网上一顿冲浪之后，看到了Kent C. Dodds的一篇博客<span class="exturl" data-url="aHR0cHM6Ly9rZW50Y2RvZGRzLmNvbS9ibG9nL2dldC1hLWNhdGNoLWJsb2NrLWVycm9yLW1lc3NhZ2Utd2l0aC10eXBlc2NyaXB0">Get a catch block error message with TypeScript<i class="fa fa-external-link-alt"></i></span>。我跟着文章的内容成功解决了这个问题，并且解答了我的疑惑，所以想要翻译出来帮助到更多的人。</p><p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失或功能异常）负责。</p><span id="more"></span><hr><p>好吧，咱们看看这个代码：</p><figure class="highlight javascript"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">reportError</span> = (<span class="params">&#123; message &#125;</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 把错误信息发给我们的日志服务...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; <span class="attr">message</span>: error.<span class="property">message</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这么写应该足够好了吧？嘛，毕竟这是JavaScript。要是换成TypeScript的话：</p><figure class="highlight typescript"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">reportError</span> = (<span class="params">&#123; message &#125;: &#123; message: <span class="built_in">string</span> &#125;</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 把错误信息发给我们的日志服务...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; <span class="attr">message</span>: error.<span class="property">message</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这时候<code>reportError</code>的<code>error.message</code>这部分就要报错了。因为（就在最近）TypeScript把<code>error</code>的类型定义成了<code>unknown</code>。这倒也是事实，因为它确实没法保证抛出的错误的类型。哦对，这也是你不能用promise的泛型(<code>Promise&lt;ResolvedValue, NopeYouCantProvideARejectedValueType&gt;</code>)，来给promise reject的<code>.catch(error =&gt; {})</code>指定类型的原因。而且，被抛出来的东西可能都不是一个error，它可以是任何东西：</p><figure class="highlight typescript"><table><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">throw</span> <span class="string">&#x27;啥玩意？！&#x27;</span> <span class="comment">// 译者注：原文为 &#x27;What the!?&#x27;，请尝试用东北口音理解</span></span><br><span class="line"><span class="keyword">throw</span> <span class="number">7</span></span><br><span class="line"><span class="keyword">throw</span> &#123; <span class="attr">wut</span>: <span class="string">&#x27;is this&#x27;</span> &#125; <span class="comment">// 译者注：wut可以用来表达“傻眼”语境下的what，类似“什么鬼”</span></span><br><span class="line"><span class="keyword">throw</span> <span class="literal">null</span></span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">() =&gt;</span> &#123;&#125;)</span><br><span class="line"><span class="keyword">throw</span> <span class="literal">undefined</span></span><br></pre></td></tr></table></figure><p>说真的，你可以想throw啥就throw啥，啥东西都行。那，要解决上面提到的错误信息好像挺简单对吧，我们在<code>catch</code>中声明代码只会抛出error不就行了？</p><figure class="highlight typescript"><table><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">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (<span class="attr">error</span>: <span class="title class_">Error</span>) &#123;</span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; <span class="attr">message</span>: error.<span class="property">message</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>想的美！现在你会得到这么一条TypeScript的编译错误：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Catch clause variable type annotation must be &#x27;any&#x27; or &#x27;unknown&#x27; if specified. ts(1196)</span><br></pre></td></tr></table></figure><p>报这个错的原因是，尽管看起来在我们的代码里不可能会抛出来其他的东西，但JavaScript就这么逗，一个第三方库完全有可能做点什么奇怪的事，比如给Error的构造函数来个猴子补丁（译者注：monkey-patching），让它抛出点不一样的东西：</p><figure class="highlight typescript"><table><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="title class_">Error</span> = <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="string">&#x27;Flowers&#x27;</span></span><br><span class="line">&#125; <span class="keyword">as</span> <span class="built_in">any</span></span><br></pre></td></tr></table></figure><p>那咱们开发者该怎么办？我们只能尽力，比如这样：</p><figure class="highlight typescript"><table><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="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">let</span> message = <span class="string">&#x27;Unknown Error&#x27;</span></span><br><span class="line">    <span class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">Error</span>) message = error.<span class="property">message</span></span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; message &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>妥了这不！现在TypeScript也不跟我们嚷嚷有问题了，而且万一这个error是什么奇怪的东西，我们也用了合适的办法来处理它了。而且这段代码我们还能继续优化成这样：</p><figure class="highlight typescript"><table><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">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="keyword">let</span> message</span><br><span class="line">    <span class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">Error</span>) message = error.<span class="property">message</span></span><br><span class="line">    <span class="keyword">else</span> message = <span class="title class_">String</span>(error)</span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; message &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么现在，如果这个error不是一个<code>Error</code>对象，那么我们就直接把它变成一个字符串并祈祷这个错误信息能有点用。</p><p>然后，我们还能把这段代码抽出来做成一个工具方法来给所有的catch块用：</p><figure class="highlight typescript"><table><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="keyword">function</span> <span class="title function_">getErrorMessage</span>(<span class="params"><span class="attr">error</span>: <span class="built_in">unknown</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (error <span class="keyword">instanceof</span> <span class="title class_">Error</span>) <span class="keyword">return</span> error.<span class="property">message</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">String</span>(error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">reportError</span> = (<span class="params">&#123; message &#125;: &#123; message: <span class="built_in">string</span> &#125;</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 把错误信息发给我们的日志服务...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Oh no!&#x27;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="comment">// 我们会让代码继续执行，但先把错误报告出去</span></span><br><span class="line">    <span class="title function_">reportError</span>(&#123; <span class="attr">message</span>: <span class="title function_">getErrorMessage</span>(error) &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个写法在我的项目里面特别好用。希望也能帮助到你！</p><p>更新：<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL25waXJvdHRl">Nicolas<i class="fa fa-external-link-alt"></i></span>针对error对象并不真的是error的情况提了一个<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2tlbnRjZG9kZHMva2VudGNkb2Rkcy5jb20vaXNzdWVzLzIwNg==">很好的建议<i class="fa fa-external-link-alt"></i></span>。此外<span class="exturl" data-url="aHR0cHM6Ly9kaXNjb3JkLmNvbS91c2Vycy84MDQ3OTU2NTIyNTIxMDY3NjI=">Jesse<i class="fa fa-external-link-alt"></i></span>也提出了<span class="exturl" data-url="aHR0cHM6Ly9kaXNjb3JkLmNvbS9jaGFubmVscy83MTUyMjA3MzA2MDU3MzE5MzEvNzE1MjI3NzM5NzQ5MDg5MjgxLzkwMzY0OTMxMzIyODQ4MDUzMg==">一个建议<i class="fa fa-external-link-alt"></i></span>，在可能的情况下把error对象也转换成字符串。把这些结合起来，我们就得到了这样的一份代码：</p><figure class="highlight typescript"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ErrorWithMessage</span> = &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="built_in">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isErrorWithMessage</span>(<span class="params"><span class="attr">error</span>: <span class="built_in">unknown</span></span>): error is <span class="title class_">ErrorWithMessage</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="keyword">typeof</span> error === <span class="string">&#x27;object&#x27;</span> &amp;&amp;</span><br><span class="line">        error !== <span class="literal">null</span> &amp;&amp;</span><br><span class="line">        <span class="string">&#x27;message&#x27;</span> <span class="keyword">in</span> error &amp;&amp;</span><br><span class="line">        <span class="title function_">typeof</span> (error <span class="keyword">as</span> <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">unknown</span>&gt;).<span class="property">message</span> === <span class="string">&#x27;string&#x27;</span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">toErrorWithMessage</span>(<span class="params"><span class="attr">maybeError</span>: <span class="built_in">unknown</span></span>): <span class="title class_">ErrorWithMessage</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">isErrorWithMessage</span>(maybeError)) <span class="keyword">return</span> maybeError</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(maybeError))</span><br><span class="line">    &#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">        <span class="comment">// fallback in case there&#x27;s an error stringifying the maybeError</span></span><br><span class="line">        <span class="comment">// like with circular references for example.</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="title class_">String</span>(maybeError))</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getErrorMessage</span>(<span class="params"><span class="attr">error</span>: <span class="built_in">unknown</span></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">toErrorWithMessage</span>(error).<span class="property">message</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>简直太好使了！</p><p><strong>结论</strong></p><p>我觉得关键在于，尽管TypeScript有一些奇怪的地方，但也绝对不要因为你觉得这不可能就忽略TypeScript抛出的编译错误或警告。大多数情况下，意外是非常有可能发生的，而TypeScript很好的强制你去处理这些“不太可能发生”的情况……然后你也很有可能会发现，这些情况并没有你想的那么少见。</p><hr><p>译者的碎碎念：作为一个TypeScript纯新手的我，最后这段代码给我看傻了。Java里面非常常见的<code>try {...} catch (Exception e) {...}</code>在TypeScript里面竟然能玩这么花……</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>用裸git仓库管理点文件的最佳方式</title>
      <link>https://www.boris1993.com/translation-dotfiles-best-way-to-store-in-a-bare-git-repository.html</link>
      <description>
        <![CDATA[<p>前些天在Atlassian的文档中看到一篇关于管理点文件（即<code>.bashrc</code>之类以点开头的文件）的文章<span class="exturl" data-url="aHR0cHM6Ly93d3cuYXRsYXNzaWFuLmNvbS9naXQvdHV0b3JpYWxzL2RvdGZpbGVz">《Dotfiles: Best way to store in a bare git repository》<i class="fa fa-external-link-alt"></i></span>，感觉很有参考价值，遂决定翻译出来，希望能帮到更多的人。</p>
<p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失）负责。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/translations/">翻译</category>
      <category domain="https://www.boris1993.com/tags/git/">git</category>
      <category domain="https://www.boris1993.com/tags/dotfiles/">dotfiles</category>
      <category domain="https://www.boris1993.com/tags/%E7%82%B9%E6%96%87%E4%BB%B6/">点文件</category>
      <pubDate>Thu, 09 May 2024 13:13:41 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>前些天在Atlassian的文档中看到一篇关于管理点文件（即<code>.bashrc</code>之类以点开头的文件）的文章<span class="exturl" data-url="aHR0cHM6Ly93d3cuYXRsYXNzaWFuLmNvbS9naXQvdHV0b3JpYWxzL2RvdGZpbGVz">《Dotfiles: Best way to store in a bare git repository》<i class="fa fa-external-link-alt"></i></span>，感觉很有参考价值，遂决定翻译出来，希望能帮到更多的人。</p><p>以下内容除特别注明外，皆翻译自原文。我亦不对内容做任何的担保，并不对任何可能产生的后果（包括但不限于文件丢失）负责。</p><span id="more"></span><hr><p>免责声明：标题有些夸大其词了，而且针对这个问题也有其他好用的解决方案。但我确实觉得这也是一个优雅的技巧。</p><p>最近我在Hacker News中看到一篇<span class="exturl" data-url="aHR0cHM6Ly9uZXdzLnljb21iaW5hdG9yLmNvbS9pdGVtP2lkPTExMDcwNzk3">帖子<i class="fa fa-external-link-alt"></i></span>，是讨论人们怎么管理他们的<span class="exturl" data-url="aHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRG90LWZpbGU=">点文件<i class="fa fa-external-link-alt"></i></span>的。在这篇帖子中，用户<code>StreakyCobra</code>分享了一个他的<span class="exturl" data-url="aHR0cHM6Ly9uZXdzLnljb21iaW5hdG9yLmNvbS9pdGVtP2lkPTExMDcxNzU0">很优雅的配置<i class="fa fa-external-link-alt"></i></span>，而且我觉得非常的靠谱。正巧我也在开始用同样的技巧来管理我的系统。这个技巧也只有一个前置条件：你安装了<span class="exturl" data-url="aHR0cHM6Ly93d3cuYXRsYXNzaWFuLmNvbS9naXQ=">Git<i class="fa fa-external-link-alt"></i></span>。</p><p>按他的说法，这个技巧：</p><blockquote><p>不需要别的工具，不需要创建符号链接，文件都被版本控制系统跟踪，你可以用不同的分支来管理不同的系统，在全新的系统中你也可以轻松复用你的配置。</p></blockquote><p>这个技巧包括两个部分：用一个单独的文件夹（如<code>$HOME/.cfg</code>或<code>$HOME/.myconfig</code>）作为<span class="exturl" data-url="aHR0cDovL3d3dy5zYWludHNqZC5jb20vMjAxMS8wMS93aGF0LWlzLWEtYmFyZS1naXQtcmVwb3NpdG9yeS8=">裸Git仓库<i class="fa fa-external-link-alt"></i></span>；和一个专门用来操作这个仓库的命令别名（译者注：<code>alias</code>）。</p><h2 id="从零开始"><a href="#从零开始" class="headerlink" title="从零开始"></a>从零开始</h2><p>如果你还没有用一个Git仓库来跟踪你的配置文件，那么你可以从下面这几个简单的命令开始，一步步实现这个技巧。</p><figure class="highlight bash"><table><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">git init --bare <span class="variable">$HOME</span>/.cfg</span><br><span class="line"><span class="built_in">alias</span> config=<span class="string">&#x27;/usr/bin/git --git-dir=$HOME/.cfg/ --work-tree=$HOME&#x27;</span></span><br><span class="line">config config --<span class="built_in">local</span> status.showUntrackedFiles no</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;alias config=&#x27;/usr/bin/git --git-dir=<span class="variable">$HOME</span>/.cfg/ --work-tree=<span class="variable">$HOME</span>&#x27;&quot;</span> &gt;&gt; <span class="variable">$HOME</span>/.bashrc</span><br></pre></td></tr></table></figure><ul><li>第一条命令会创建一个用来跟踪文件的裸Git仓库<code>~/.cfg</code>（配置仓库）</li><li>接下来创建一个命令别名<code>config</code>，在我们要操作配置仓库时就会用这个命令，而不是<code>git</code>命令</li><li>设定一个仅对配置仓库生效的设定 —— 不显示未追踪的文件，这样在我们执行<code>config status</code>等命令的时候，我们不想跟踪的文件就不会以<code>untracked</code>状态被显示出来</li><li>此外我们可以把这个别名的声明添加到<code>.bashrc</code>里面，方便以后使用</li></ul><p>我把上面这些命令集合成了一个<span class="exturl" data-url="aHR0cHM6Ly9iaXRidWNrZXQub3JnL3NuaXBwZXRzL25pY29sYXBhb2x1Y2NpL2VyZ1g5">代码片段<i class="fa fa-external-link-alt"></i></span>放到了Bitbucket上，并给它做了一个短链接，所以你可以用下面的命令一键执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -Lks http://bit.do/cfg-init | /bin/bash</span><br></pre></td></tr></table></figure><p>（译者注：在执行来自网络的代码之前，一定记得先看看内容。闭著眼执行远程命令是一件很危险的事，因为你不知道这个链接的内容是否还正确，比如曾经课本上本来指向《历朝历代咏武侯诗词大全》的网址，在域名过期之后被黄网搞去了……）</p><p>在配置完成后，<code>$HOME</code>目录下的文件就可以用刚刚创建的<code>config</code>别名来进行版本控制了，比如：</p><figure class="highlight bash"><table><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">config status</span><br><span class="line">config add .vimrc</span><br><span class="line">config commit -m <span class="string">&quot;Add .vimrc&quot;</span></span><br><span class="line">config add .bashrc</span><br><span class="line">config commit -m <span class="string">&quot;Add .bashrc&quot;</span></span><br><span class="line">config push</span><br></pre></td></tr></table></figure><h2 id="把这些点文件安装到新系统（或迁移到本系统）"><a href="#把这些点文件安装到新系统（或迁移到本系统）" class="headerlink" title="把这些点文件安装到新系统（或迁移到本系统）"></a>把这些点文件安装到新系统（或迁移到本系统）</h2><p>如果你已经用Git仓库管理你的配置或者点文件，那你可以跟着下面的步骤，来把这个配置迁移到新的系统：</p><ul><li>首先确认你已经配置好了命令别名</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">alias</span> config=<span class="string">&#x27;/usr/bin/git --git-dir=$HOME/.cfg/ --work-tree=$HOME&#x27;</span></span><br></pre></td></tr></table></figure><ul><li>在克隆你的配置仓库前，记得让git忽略它，以避免各种奇怪的问题</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;.cfg&quot;</span> &gt;&gt; .gitignore</span><br></pre></td></tr></table></figure><ul><li>然后你就可以把你的点文件克隆到一个存在于“点目录”的裸仓库中</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> --bare &lt;git-repo-url&gt; <span class="variable">$HOME</span>/.cfg</span><br></pre></td></tr></table></figure><ul><li>在当前shell会话中定义好<code>config</code>这个别名</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">alias</span> config=<span class="string">&#x27;/usr/bin/git --git-dir=$HOME/.cfg/ --work-tree=$HOME&#x27;</span></span><br></pre></td></tr></table></figure><ul><li>从裸仓库中把配置checkout到<code>$HOME</code></li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">config checkout</span><br></pre></td></tr></table></figure><p>这一步可能会报这样的错误</p><figure class="highlight text"><table><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">error: The following untracked working tree files would be overwritten by checkout:</span><br><span class="line">    .bashrc</span><br><span class="line">    .gitignore</span><br><span class="line">Please move or remove them before you can switch branches.</span><br><span class="line">Aborting</span><br></pre></td></tr></table></figure><p>这是因为你的<code>$HOME</code>目录可能已经有这些文件，而checkout操作可能会让它们被覆盖。解决方法也很简单：这些文件要是有用，那就备份出来，没用那就删了。你可以用我这个比较粗糙的命令，来一键把有冲突的文件移动到备份目录：</p><figure class="highlight bash"><table><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="built_in">mkdir</span> -p .config-backup &amp;&amp; \</span><br><span class="line">config checkout 2&gt;&amp;1 | egrep <span class="string">&quot;\s+\.&quot;</span> | awk &#123;<span class="string">&#x27;print $1&#x27;</span>&#125; | \</span><br><span class="line">xargs -I&#123;&#125; <span class="built_in">mv</span> &#123;&#125; .config-backup/&#123;&#125;</span><br></pre></td></tr></table></figure><p>然后重新执行<code>config checkout</code></p><ul><li>为配置仓库设定不显示未跟踪的文件</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">config config --<span class="built_in">local</span> status.showUntrackedFiles no</span><br></pre></td></tr></table></figure><ul><li>到此就完成了，接下来你就可以和之前一样用<code>config</code>命令来管理你的点文件了。</li></ul><p>同样，我也提供了一个一键脚本，把它作为代码片段放在了Bitbucket上。你可以用这个命令来执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -Lks http://bit.do/cfg-install | /bin/bash</span><br></pre></td></tr></table></figure><p>为了文章的完整起见，这是我最终得到的脚本（已经在很多个全新启动的Alpine Linux容器中做过测试）</p><figure class="highlight bash"><table><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">git <span class="built_in">clone</span> --bare https://bitbucket.org/durdn/cfg.git <span class="variable">$HOME</span>/.cfg</span><br><span class="line"><span class="keyword">function</span> config &#123;</span><br><span class="line">   /usr/bin/git --git-dir=<span class="variable">$HOME</span>/.cfg/ --work-tree=<span class="variable">$HOME</span> <span class="variable">$@</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">mkdir</span> -p .config-backup</span><br><span class="line">config checkout</span><br><span class="line"><span class="keyword">if</span> [ $? = 0 ]; <span class="keyword">then</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Checked out config.&quot;</span>;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Backing up pre-existing dot files.&quot;</span>;</span><br><span class="line">  config checkout 2&gt;&amp;1 | egrep <span class="string">&quot;\s+\.&quot;</span> | awk &#123;<span class="string">&#x27;print $1&#x27;</span>&#125; | xargs -I&#123;&#125; <span class="built_in">mv</span> &#123;&#125; .config-backup/&#123;&#125;</span><br><span class="line"><span class="keyword">fi</span>;</span><br><span class="line">config checkout</span><br><span class="line">config config status.showUntrackedFiles no</span><br></pre></td></tr></table></figure><p>（译者注：原文的<code>if-else</code>部分似乎缩进有问题，我调整了下格式，未对脚本内容做改动）</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>我希望你们能觉得这个管理点文件的小技巧能产生帮助。如果你有兴趣的话，可以到<span class="exturl" data-url="aHR0cHM6Ly9iaXRidWNrZXQub3JnL2R1cmRuL2NmZy5naXQ=">这里看我的点文件<i class="fa fa-external-link-alt"></i></span>。另外别忘了关注<span class="exturl" data-url="aHR0cHM6Ly93d3cudHdpdHRlci5jb20vZHVyZG4=">@durdn<i class="fa fa-external-link-alt"></i></span>或我那炫酷的小组<span class="exturl" data-url="aHR0cHM6Ly93d3cudHdpdHRlci5jb20vYXRsYXNzaWFuZGV2">@atlassiandev<i class="fa fa-external-link-alt"></i></span>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用Homebrew Bundle管理电脑上安装的软件</title>
      <link>https://www.boris1993.com/macos-manage-softwares-with-homebrew-bundle.html</link>
      <description>
        <![CDATA[<p>今天看到<span class="exturl" data-url="aHR0cHM6Ly94LmNvbS92aWtpbmdtdXRlL3N0YXR1cy8xNzY5MTc0MzM3NzU0NjczMzI0">一篇推文<i class="fa fa-external-link-alt"></i></span>说Homebrew也支持类似<code>package.json</code>的方式来管理电脑上安装的应用，感觉这玩意挺实用的，像我上回换电脑还是照着<code>brew list</code>的输出再一个个装，有了它的话就可以一键安装了。而且除了Homebrew之外，它还可以导出通过App Store安装的软件和VS Code插件。那么，就小小折腾一下。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/playing/">瞎折腾</category>
      <category domain="https://www.boris1993.com/tags/macOS/">macOS</category>
      <category domain="https://www.boris1993.com/tags/Homebrew/">Homebrew</category>
      <pubDate>Mon, 18 Mar 2024 14:30:37 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>今天看到<span class="exturl" data-url="aHR0cHM6Ly94LmNvbS92aWtpbmdtdXRlL3N0YXR1cy8xNzY5MTc0MzM3NzU0NjczMzI0">一篇推文<i class="fa fa-external-link-alt"></i></span>说Homebrew也支持类似<code>package.json</code>的方式来管理电脑上安装的应用，感觉这玩意挺实用的，像我上回换电脑还是照着<code>brew list</code>的输出再一个个装，有了它的话就可以一键安装了。而且除了Homebrew之外，它还可以导出通过App Store安装的软件和VS Code插件。那么，就小小折腾一下。</p><span id="more"></span><p>首次运行<code>brew bundle</code>的时候它就会自动安装了。如果要导出App Store安装的程序的话，那么还需要安装<code>mas</code>这个Formula。</p><p>然后就可以执行<code>brew bundle dump</code>，它会在当前目录下生成一个<code>Brewfile</code>文件，里面就是你电脑上目前装的软件，内容就像这样：</p><figure class="highlight ruby"><table><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="comment"># 各个Homebrew仓库</span></span><br><span class="line">tap <span class="string">&quot;beeftornado/rmtree&quot;</span></span><br><span class="line">tap <span class="string">&quot;bell-sw/liberica&quot;</span></span><br><span class="line">tap <span class="string">&quot;homebrew/autoupdate&quot;</span></span><br><span class="line">tap <span class="string">&quot;homebrew/bundle&quot;</span></span><br><span class="line">tap <span class="string">&quot;homebrew/cask-fonts&quot;</span></span><br><span class="line">tap <span class="string">&quot;homebrew/cask-versions&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 通过Homebrew安装的Formula</span></span><br><span class="line">brew <span class="string">&quot;cloudflared&quot;</span></span><br><span class="line">brew <span class="string">&quot;curl&quot;</span></span><br><span class="line">brew <span class="string">&quot;git&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 通过Homebrew安装的Cask</span></span><br><span class="line">cask <span class="string">&quot;1password&quot;</span></span><br><span class="line">cask <span class="string">&quot;alfred&quot;</span></span><br><span class="line">cask <span class="string">&quot;altserver&quot;</span></span><br><span class="line">cask <span class="string">&quot;arc&quot;</span></span><br><span class="line">cask <span class="string">&quot;bartender&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 通过App Store安装的软件</span></span><br><span class="line">mas <span class="string">&quot;DaVinci Resolve&quot;</span>, <span class="symbol">id:</span> <span class="number">571213070</span></span><br><span class="line">mas <span class="string">&quot;Eul&quot;</span>, <span class="symbol">id:</span> <span class="number">1541991958</span></span><br><span class="line">mas <span class="string">&quot;Termius&quot;</span>, <span class="symbol">id:</span> <span class="number">1176074088</span></span><br><span class="line">mas <span class="string">&quot;WireGuard&quot;</span>, <span class="symbol">id:</span> <span class="number">1451685025</span></span><br><span class="line">mas <span class="string">&quot;Xnip&quot;</span>, <span class="symbol">id:</span> <span class="number">1221250572</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># VS Code插件</span></span><br><span class="line">vscode <span class="string">&quot;ms-python.debugpy&quot;</span></span><br><span class="line">vscode <span class="string">&quot;ms-python.python&quot;</span></span><br><span class="line">vscode <span class="string">&quot;redhat.vscode-yaml&quot;</span></span><br></pre></td></tr></table></figure><p>我的这个Brewfile算是简单的，它还支持配置安装参数等，具体可以参考<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL0hvbWVicmV3L2hvbWVicmV3LWJ1bmRsZS8/dGFiPXJlYWRtZS1vdi1maWxlI3VzYWdl">Homebrew Bundle的实例<i class="fa fa-external-link-alt"></i></span>。</p><p>如果在导出的时候在当前目录已经存在一个<code>Brewfile</code>，那么它会报错说文件已存在。这时候可以加<code>--force</code>参数强制导出，要在cron里定时导出的话这个参数会很有用。此外如果你想要每个软件的简介，那么还可以加上<code>--describe</code>参数，出来的<code>Brewfile</code>就会是这样的：</p><figure class="highlight ruby"><table><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="comment"># Mac App Store command-line interface</span></span><br><span class="line">brew <span class="string">&quot;mas&quot;</span></span><br><span class="line"><span class="comment"># Java-based project management</span></span><br><span class="line">brew <span class="string">&quot;maven&quot;</span></span><br><span class="line"><span class="comment"># Menu bar icon organiser</span></span><br><span class="line">cask <span class="string">&quot;bartender&quot;</span></span><br></pre></td></tr></table></figure><p>不过<code>--describe</code>参数只对<code>brew</code>和<code>cask</code>条目生效，<code>tap</code>、<code>mas</code>和<code>vscode</code>条目都是没有描述的。</p><p>我试用后发现，似乎<code>mas</code>这部分会把你已经卸载的软件也列出来，而第二次再dump的时候又另外发现了一个之前没列出来而已经通过App Store安装的程序，所以这部分可能需要在导出之后自己再人工校验一下。</p><p>要按照<code>Brewfile</code>安装软件的话，只需要cd到<code>Brewfile</code>所在位置然后执行<code>brew bundle</code>就会自动开始安装。</p><p>如果要把不在<code>Brewfile</code>中的软件删除，那么可以运行<code>brew bundle cleanup</code>命令。</p><p>此外，运行<code>brew bundle check</code>可以检查哪些软件在<code>Brewfile</code>里列出来了，但是还没安装。但是我用刚刚导出的<code>Brewfile</code>检查却报了<code>brew bundle can&#39;t satisfy your Brewfile&#39;s dependencies.Satisfy missing dependencies with ``brew bundle install``.</code>这样的消息，我就有点怀疑这个检查到底准不准……</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>理解Java中的抽象队列同步器（AQS）</title>
      <link>https://www.boris1993.com/java-understand-aqs.html</link>
      <description>
        <![CDATA[<p>最近项目里用到了些Lock，爬了些文了解到它们是基于<code>AbstractQueuedSynchronizer</code>（即<code>AQS</code>）实现的。那么，不如趁热打铁，看看里面是怎么工作的。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/Java/">Java</category>
      <category domain="https://www.boris1993.com/tags/Lock/">Lock</category>
      <category domain="https://www.boris1993.com/tags/AQS/">AQS</category>
      <category domain="https://www.boris1993.com/tags/%E9%94%81/">锁</category>
      <pubDate>Wed, 17 Jan 2024 06:46:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近项目里用到了些Lock，爬了些文了解到它们是基于<code>AbstractQueuedSynchronizer</code>（即<code>AQS</code>）实现的。那么，不如趁热打铁，看看里面是怎么工作的。</p><span id="more"></span><h2 id="什么是AQS"><a href="#什么是AQS" class="headerlink" title="什么是AQS"></a>什么是AQS</h2><p><code>AbstractQueuedSynchronizer</code>，抽象队列同步器，是很多同步器（如<code>ReentrantLock</code>、<code>CountDownLatch</code>、<code>Semaphore</code>）等都是基于它实现的。</p><p>在AQS内部，它维护了一个FIFO队列，和一个<code>volatile</code>类型的变量<code>state</code>。FIFO队列用来实现多线程的排队工作，线程加锁失败时，这个线程就会被封装成一个<code>Node</code>节点放到队尾，然后当锁被释放后，队列头部的线程就会被唤醒并让它重新尝试获取锁；<code>state</code>变量用来记录锁的状态，如<code>Semaphore</code>的<code>permit</code>就是存在<code>state</code>里面的。</p><h2 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h2><p>上面说到，AQS使用一个<code>volatile</code>的<code>int</code>变量<code>state</code>来管理锁的状态，<code>state</code>为0时说明锁被释放，反之锁被持有。</p><p>AQS提供了三个方法来同步锁的状态：<code>getState()</code>，<code>setState(int newState)</code>和<code>compareAndSetState(int expect, int update)</code>。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * The synchronization state.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">int</span> state;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Returns the current value of synchronization state.</span></span><br><span class="line"><span class="comment"> * This operation has memory semantics of a &#123;<span class="doctag">@code</span> volatile&#125; read.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> current state value</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getState</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Sets the value of synchronization state.</span></span><br><span class="line"><span class="comment"> * This operation has memory semantics of a &#123;<span class="doctag">@code</span> volatile&#125; write.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> newState the new state value</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">setState</span><span class="params">(<span class="type">int</span> newState)</span> &#123;</span><br><span class="line">    state = newState;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Atomically sets synchronization state to the given updated</span></span><br><span class="line"><span class="comment"> * value if the current state value equals the expected value.</span></span><br><span class="line"><span class="comment"> * This operation has memory semantics of a &#123;<span class="doctag">@code</span> volatile&#125; read</span></span><br><span class="line"><span class="comment"> * and write.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> expect the expected value</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> update the new value</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> &#123;<span class="doctag">@code</span> true&#125; if successful. False return indicates that the actual</span></span><br><span class="line"><span class="comment"> *         value was not equal to the expected value.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">compareAndSetState</span><span class="params">(<span class="type">int</span> expect, <span class="type">int</span> update)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> U.compareAndSetInt(<span class="built_in">this</span>, STATE, expect, update);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>查看<code>setState</code>方法的引用，不难发现像<code>CountDownLatch</code>和<code>Semaphore</code>这些熟悉的身影。</p><p><img data-src="https://blog-static.boris1993.com/java-understand-aqs/usage-of-set-state.png"></p><h2 id="FIFO队列-线程排队等待锁的地方"><a href="#FIFO队列-线程排队等待锁的地方" class="headerlink" title="FIFO队列 - 线程排队等待锁的地方"></a>FIFO队列 - 线程排队等待锁的地方</h2><p>在AQS内部，未能成功获取锁的线程都会被包装成一个<code>Node</code>节点，然后放到FIFO队列尾部让它等待。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Node status bits, also used as argument and return values</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">WAITING</span>   <span class="operator">=</span> <span class="number">1</span>;          <span class="comment">// must be 1</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CANCELLED</span> <span class="operator">=</span> <span class="number">0x80000000</span>; <span class="comment">// must be negative</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">COND</span>      <span class="operator">=</span> <span class="number">2</span>;          <span class="comment">// in a condition wait</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    <span class="keyword">volatile</span> Node prev;       <span class="comment">// initially attached via casTail</span></span><br><span class="line">    <span class="keyword">volatile</span> Node next;       <span class="comment">// visibly nonnull when signallable</span></span><br><span class="line">    Thread waiter;            <span class="comment">// visibly nonnull when enqueued</span></span><br><span class="line">    <span class="keyword">volatile</span> <span class="type">int</span> status;      <span class="comment">// written by owner, atomic bit ops by others</span></span><br><span class="line">    <span class="comment">// 略</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Head of the wait queue, lazily initialized.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">transient</span> <span class="keyword">volatile</span> Node head;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Tail of the wait queue. After initialization, modified only via casTail.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">transient</span> <span class="keyword">volatile</span> Node tail;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Enqueues the node unless null. (Currently used only for</span></span><br><span class="line"><span class="comment"> * ConditionNodes; other cases are interleaved with acquires.)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">enqueue</span><span class="params">(Node node)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (node != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">            <span class="type">Node</span> <span class="variable">t</span> <span class="operator">=</span> tail;</span><br><span class="line">            node.setPrevRelaxed(t);        <span class="comment">// avoid unnecessary fence</span></span><br><span class="line">            <span class="keyword">if</span> (t == <span class="literal">null</span>)                 <span class="comment">// initialize</span></span><br><span class="line">                tryInitializeHead();</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (casTail(t, node)) &#123;</span><br><span class="line">                t.next = node;</span><br><span class="line">                <span class="keyword">if</span> (t.status &lt; <span class="number">0</span>)          <span class="comment">// wake up to clean link</span></span><br><span class="line">                    LockSupport.unpark(node.waiter);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Semaphore"><a href="#Semaphore" class="headerlink" title="Semaphore"></a>Semaphore</h2><p><code>Semaphore</code>就是AQS的一个实现，从它的源码就能很容易看出来，它内部就是通过AQS的<code>state</code>来管理<code>permits</code>。</p><figure class="highlight java"><table><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><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</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_">Semaphore</span> <span class="keyword">implements</span> <span class="title class_">java</span>.io.Serializable &#123;</span><br><span class="line">    <span class="comment">/** All mechanics via AbstractQueuedSynchronizer subclass */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Sync sync;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Synchronization implementation for semaphore.  Uses AQS state</span></span><br><span class="line"><span class="comment">     * to represent permits. Subclassed into fair and nonfair</span></span><br><span class="line"><span class="comment">     * versions.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">abstract</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Sync</span> <span class="keyword">extends</span> <span class="title class_">AbstractQueuedSynchronizer</span> &#123;</span><br><span class="line">        Sync(<span class="type">int</span> <span class="keyword">permits</span>) &#123;</span><br><span class="line">            setState(<span class="keyword">permits</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getPermits</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> getState();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">nonfairTryAcquireShared</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">available</span> <span class="operator">=</span> getState();</span><br><span class="line">                <span class="type">int</span> <span class="variable">remaining</span> <span class="operator">=</span> available - acquires;</span><br><span class="line">                <span class="keyword">if</span> (remaining &lt; <span class="number">0</span> ||</span><br><span class="line">                    compareAndSetState(available, remaining))</span><br><span class="line">                    <span class="keyword">return</span> remaining;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">tryReleaseShared</span><span class="params">(<span class="type">int</span> releases)</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">current</span> <span class="operator">=</span> getState();</span><br><span class="line">                <span class="type">int</span> <span class="variable">next</span> <span class="operator">=</span> current + releases;</span><br><span class="line">                <span class="keyword">if</span> (next &lt; current) <span class="comment">// overflow</span></span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;Maximum permit count exceeded&quot;</span>);</span><br><span class="line">                <span class="keyword">if</span> (compareAndSetState(current, next))</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">reducePermits</span><span class="params">(<span class="type">int</span> reductions)</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">current</span> <span class="operator">=</span> getState();</span><br><span class="line">                <span class="type">int</span> <span class="variable">next</span> <span class="operator">=</span> current - reductions;</span><br><span class="line">                <span class="keyword">if</span> (next &gt; current) <span class="comment">// underflow</span></span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;Permit count underflow&quot;</span>);</span><br><span class="line">                <span class="keyword">if</span> (compareAndSetState(current, next))</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">drainPermits</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">current</span> <span class="operator">=</span> getState();</span><br><span class="line">                <span class="keyword">if</span> (current == <span class="number">0</span> || compareAndSetState(current, <span class="number">0</span>))</span><br><span class="line">                    <span class="keyword">return</span> current;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * NonFair version</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">NonfairSync</span> <span class="keyword">extends</span> <span class="title class_">Sync</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> -<span class="number">2694183684443567898L</span>;</span><br><span class="line"></span><br><span class="line">        NonfairSync(<span class="type">int</span> <span class="keyword">permits</span>) &#123;</span><br><span class="line">            <span class="built_in">super</span>(<span class="keyword">permits</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">protected</span> <span class="type">int</span> <span class="title function_">tryAcquireShared</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> nonfairTryAcquireShared(acquires);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Fair version</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">FairSync</span> <span class="keyword">extends</span> <span class="title class_">Sync</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> <span class="number">2014338818796000944L</span>;</span><br><span class="line"></span><br><span class="line">        FairSync(<span class="type">int</span> <span class="keyword">permits</span>) &#123;</span><br><span class="line">            <span class="built_in">super</span>(<span class="keyword">permits</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">protected</span> <span class="type">int</span> <span class="title function_">tryAcquireShared</span><span class="params">(<span class="type">int</span> acquires)</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="keyword">if</span> (hasQueuedPredecessors())</span><br><span class="line">                    <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">                <span class="type">int</span> <span class="variable">available</span> <span class="operator">=</span> getState();</span><br><span class="line">                <span class="type">int</span> <span class="variable">remaining</span> <span class="operator">=</span> available - acquires;</span><br><span class="line">                <span class="keyword">if</span> (remaining &lt; <span class="number">0</span> ||</span><br><span class="line">                    compareAndSetState(available, remaining))</span><br><span class="line">                    <span class="keyword">return</span> remaining;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Creates a &#123;<span class="doctag">@code</span> Semaphore&#125; with the given number of</span></span><br><span class="line"><span class="comment">     * permits and nonfair fairness setting.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> permits the initial number of permits available.</span></span><br><span class="line"><span class="comment">     *        This value may be negative, in which case releases</span></span><br><span class="line"><span class="comment">     *        must occur before any acquires will be granted.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Semaphore</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>)</span> &#123;</span><br><span class="line">        sync = <span class="keyword">new</span> <span class="title class_">NonfairSync</span>(<span class="keyword">permits</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Creates a &#123;<span class="doctag">@code</span> Semaphore&#125; with the given number of</span></span><br><span class="line"><span class="comment">     * permits and the given fairness setting.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> permits the initial number of permits available.</span></span><br><span class="line"><span class="comment">     *        This value may be negative, in which case releases</span></span><br><span class="line"><span class="comment">     *        must occur before any acquires will be granted.</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> fair &#123;<span class="doctag">@code</span> true&#125; if this semaphore will guarantee</span></span><br><span class="line"><span class="comment">     *        first-in first-out granting of permits under contention,</span></span><br><span class="line"><span class="comment">     *        else &#123;<span class="doctag">@code</span> false&#125;</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Semaphore</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>, <span class="type">boolean</span> fair)</span> &#123;</span><br><span class="line">        sync = fair ? <span class="keyword">new</span> <span class="title class_">FairSync</span>(<span class="keyword">permits</span>) : <span class="keyword">new</span> <span class="title class_">NonfairSync</span>(<span class="keyword">permits</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="与synchronized的区别"><a href="#与synchronized的区别" class="headerlink" title="与synchronized的区别"></a>与synchronized的区别</h2><ul><li><code>synchronized</code>是一个Java内置的关键字，<code>AQS</code>扩展的各种锁则是通过Java代码实现的</li><li><code>synchronzed</code>锁是自动获取和释放的，而<code>AQS</code>的锁需要手动获取和释放</li><li>像<code>ReentrantLock</code>还可以设置超时等特性，但<code>synchronized</code>不行</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Spring Boot启动过程中的后置处理</title>
      <link>https://www.boris1993.com/spring-boot-application-start-up-post-processing.html</link>
      <description>
        <![CDATA[<p>在前一篇博文<a href="/spring-boot-application-starts-up.html">Spring Boot启动流程分析</a>的<code>调用 beanFactory 的后置处理</code>小节中提到了beanFactory的后置处理。这部分是Spring IoC的重点，但是因为内容很长，所以在这里单独开一篇博文来分析。</p>]]>
      </description>
      <author>Boris Zhao</author>
      <category domain="https://www.boris1993.com/categories/learning/">学知识</category>
      <category domain="https://www.boris1993.com/tags/Java/">Java</category>
      <category domain="https://www.boris1993.com/tags/Spring-Boot/">Spring Boot</category>
      <pubDate>Sun, 14 Jan 2024 08:02:28 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在前一篇博文<a href="/spring-boot-application-starts-up.html">Spring Boot启动流程分析</a>的<code>调用 beanFactory 的后置处理</code>小节中提到了beanFactory的后置处理。这部分是Spring IoC的重点，但是因为内容很长，所以在这里单独开一篇博文来分析。</p><span id="more"></span><h2 id="调用beanFactoryPostProcessors"><a href="#调用beanFactoryPostProcessors" class="headerlink" title="调用beanFactoryPostProcessors"></a>调用beanFactoryPostProcessors</h2><p>话不多说，继续从<code>AbstractApplicationContext#refresh</code>调用<code>invokeBeanFactoryPostProcessors</code>方法这里开始。</p><figure class="highlight java"><table><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><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">invokeBeanFactoryPostProcessors</span><span class="params">(ConfigurableListableBeanFactory beanFactory)</span> &#123;</span><br><span class="line">    <span class="comment">// 重点</span></span><br><span class="line">    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime</span></span><br><span class="line">    <span class="comment">// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)</span></span><br><span class="line">    <span class="keyword">if</span> (!NativeDetector.inNativeImage() &amp;&amp; beanFactory.getTempClassLoader() == <span class="literal">null</span> &amp;&amp;</span><br><span class="line">            beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) &#123;</span><br><span class="line">        beanFactory.addBeanPostProcessor(<span class="keyword">new</span> <span class="title class_">LoadTimeWeaverAwareProcessor</span>(beanFactory));</span><br><span class="line">        beanFactory.setTempClassLoader(<span class="keyword">new</span> <span class="title class_">ContextTypeMatchClassLoader</span>(beanFactory.getBeanClassLoader()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// PostProcessorRegistrationDelegate</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_">invokeBeanFactoryPostProcessors</span><span class="params">(</span></span><br><span class="line"><span class="params">        ConfigurableListableBeanFactory beanFactory, List&lt;BeanFactoryPostProcessor&gt; beanFactoryPostProcessors)</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// WARNING: Although it may appear that the body of this method can be easily</span></span><br><span class="line">    <span class="comment">// refactored to avoid the use of multiple loops and multiple lists, the use</span></span><br><span class="line">    <span class="comment">// of multiple lists and multiple passes over the names of processors is</span></span><br><span class="line">    <span class="comment">// intentional. We must ensure that we honor the contracts for PriorityOrdered</span></span><br><span class="line">    <span class="comment">// and Ordered processors. Specifically, we must NOT cause processors to be</span></span><br><span class="line">    <span class="comment">// instantiated (via getBean() invocations) or registered in the ApplicationContext</span></span><br><span class="line">    <span class="comment">// in the wrong order.</span></span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">    <span class="comment">// Before submitting a pull request (PR) to change this method, please review the</span></span><br><span class="line">    <span class="comment">// list of all declined PRs involving changes to PostProcessorRegistrationDelegate</span></span><br><span class="line">    <span class="comment">// to ensure that your proposal does not result in a breaking change:</span></span><br><span class="line">    <span class="comment">// https://github.com/spring-projects/spring-framework/issues?q=PostProcessorRegistrationDelegate+is%3Aclosed+label%3A%22status%3A+declined%22</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Invoke BeanDefinitionRegistryPostProcessors first, if any.</span></span><br><span class="line">    Set&lt;String&gt; processedBeans = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (beanFactory <span class="keyword">instanceof</span> BeanDefinitionRegistry registry) &#123;</span><br><span class="line">        List&lt;BeanFactoryPostProcessor&gt; regularPostProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        List&lt;BeanDefinitionRegistryPostProcessor&gt; registryProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) &#123;</span><br><span class="line">            <span class="keyword">if</span> (postProcessor <span class="keyword">instanceof</span> BeanDefinitionRegistryPostProcessor registryProcessor) &#123;</span><br><span class="line">                registryProcessor.postProcessBeanDefinitionRegistry(registry);</span><br><span class="line">                registryProcessors.add(registryProcessor);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> &#123;</span><br><span class="line">                regularPostProcessors.add(postProcessor);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Do not initialize FactoryBeans here: We need to leave all regular beans</span></span><br><span class="line">        <span class="comment">// uninitialized to let the bean factory post-processors apply to them!</span></span><br><span class="line">        <span class="comment">// Separate between BeanDefinitionRegistryPostProcessors that implement</span></span><br><span class="line">        <span class="comment">// PriorityOrdered, Ordered, and the rest.</span></span><br><span class="line">        List&lt;BeanDefinitionRegistryPostProcessor&gt; currentRegistryProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.</span></span><br><span class="line">        String[] postProcessorNames =</span><br><span class="line">                beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, <span class="literal">true</span>, <span class="literal">false</span>);</span><br><span class="line">        <span class="keyword">for</span> (String ppName : postProcessorNames) &#123;</span><br><span class="line">            <span class="keyword">if</span> (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) &#123;</span><br><span class="line">                currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));</span><br><span class="line">                processedBeans.add(ppName);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        sortPostProcessors(currentRegistryProcessors, beanFactory);</span><br><span class="line">        registryProcessors.addAll(currentRegistryProcessors);</span><br><span class="line">        <span class="comment">// 这里是重点</span></span><br><span class="line">        invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());</span><br><span class="line">        currentRegistryProcessors.clear();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.</span></span><br><span class="line">        postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, <span class="literal">true</span>, <span class="literal">false</span>);</span><br><span class="line">        <span class="keyword">for</span> (String ppName : postProcessorNames) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!processedBeans.contains(ppName) &amp;&amp; beanFactory.isTypeMatch(ppName, Ordered.class)) &#123;</span><br><span class="line">                currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));</span><br><span class="line">                processedBeans.add(ppName);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        sortPostProcessors(currentRegistryProcessors, beanFactory);</span><br><span class="line">        registryProcessors.addAll(currentRegistryProcessors);</span><br><span class="line">        invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());</span><br><span class="line">        currentRegistryProcessors.clear();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.</span></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">reiterate</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">while</span> (reiterate) &#123;</span><br><span class="line">            reiterate = <span class="literal">false</span>;</span><br><span class="line">            postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, <span class="literal">true</span>, <span class="literal">false</span>);</span><br><span class="line">            <span class="keyword">for</span> (String ppName : postProcessorNames) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!processedBeans.contains(ppName)) &#123;</span><br><span class="line">                    currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));</span><br><span class="line">                    processedBeans.add(ppName);</span><br><span class="line">                    reiterate = <span class="literal">true</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            sortPostProcessors(currentRegistryProcessors, beanFactory);</span><br><span class="line">            registryProcessors.addAll(currentRegistryProcessors);</span><br><span class="line">            invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());</span><br><span class="line">            currentRegistryProcessors.clear();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Now, invoke the postProcessBeanFactory callback of all processors handled so far.</span></span><br><span class="line">        invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);</span><br><span class="line">        invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// Invoke factory processors registered with the context instance.</span></span><br><span class="line">        invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Do not initialize FactoryBeans here: We need to leave all regular beans</span></span><br><span class="line">    <span class="comment">// uninitialized to let the bean factory post-processors apply to them!</span></span><br><span class="line">    String[] postProcessorNames =</span><br><span class="line">            beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, <span class="literal">true</span>, <span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,</span></span><br><span class="line">    <span class="comment">// Ordered, and the rest.</span></span><br><span class="line">    List&lt;BeanFactoryPostProcessor&gt; priorityOrderedPostProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    List&lt;String&gt; orderedPostProcessorNames = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    List&lt;String&gt; nonOrderedPostProcessorNames = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (String ppName : postProcessorNames) &#123;</span><br><span class="line">        <span class="keyword">if</span> (processedBeans.contains(ppName)) &#123;</span><br><span class="line">            <span class="comment">// skip - already processed in first phase above</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) &#123;</span><br><span class="line">            priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (beanFactory.isTypeMatch(ppName, Ordered.class)) &#123;</span><br><span class="line">            orderedPostProcessorNames.add(ppName);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            nonOrderedPostProcessorNames.add(ppName);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.</span></span><br><span class="line">    sortPostProcessors(priorityOrderedPostProcessors, beanFactory);</span><br><span class="line">    invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Next, invoke the BeanFactoryPostProcessors that implement Ordered.</span></span><br><span class="line">    List&lt;BeanFactoryPostProcessor&gt; orderedPostProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(orderedPostProcessorNames.size());</span><br><span class="line">    <span class="keyword">for</span> (String postProcessorName : orderedPostProcessorNames) &#123;</span><br><span class="line">        orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));</span><br><span class="line">    &#125;</span><br><span class="line">    sortPostProcessors(orderedPostProcessors, beanFactory);</span><br><span class="line">    invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Finally, invoke all other BeanFactoryPostProcessors.</span></span><br><span class="line">    List&lt;BeanFactoryPostProcessor&gt; nonOrderedPostProcessors = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(nonOrderedPostProcessorNames.size());</span><br><span class="line">    <span class="keyword">for</span> (String postProcessorName : nonOrderedPostProcessorNames) &#123;</span><br><span class="line">        nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));</span><br><span class="line">    &#125;</span><br><span class="line">    invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Clear cached merged bean definitions since the post-processors might have</span></span><br><span class="line">    <span class="comment">// modified the original metadata, e.g. replacing placeholders in values...</span></span><br><span class="line">    beanFactory.clearMetadataCache();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一大段代码，实话说看得我云里雾里的，但站在巨人的肩膀上之后，我大概明白<code>invokeBeanDefinitionRegistryPostProcessors</code>这个方法调用是里面的重点，那接下来就随着巨人的指引看看这个方法。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">invokeBeanDefinitionRegistryPostProcessors</span><span class="params">(</span></span><br><span class="line"><span class="params">        Collection&lt;? extends BeanDefinitionRegistryPostProcessor&gt; postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup)</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) &#123;</span><br><span class="line">        <span class="type">StartupStep</span> <span class="variable">postProcessBeanDefRegistry</span> <span class="operator">=</span> applicationStartup.start(<span class="string">&quot;spring.context.beandef-registry.post-process&quot;</span>)</span><br><span class="line">                .tag(<span class="string">&quot;postProcessor&quot;</span>, postProcessor::toString);</span><br><span class="line">        postProcessor.postProcessBeanDefinitionRegistry(registry);</span><br><span class="line">        postProcessBeanDefRegistry.end();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>debug到<code>postProcessBeanDefinitionRegistry</code>这一行，发现<code>postProcessors</code>里面只有<code>ConfigurationClassPostProcessor</code>，那么就进去看看这个类的<code>postProcessBeanDefinitionRegistry</code>在干什么。</p><p>下面代码会大量提到configuration class这个概念，而所谓configuration class，就是带有<code>@Configuration</code>或者<code>@Component</code>注解的类。</p><figure class="highlight java"><table><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><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</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_">postProcessBeanDefinitionRegistry</span><span class="params">(BeanDefinitionRegistry registry)</span> &#123;</span><br><span class="line">    <span class="type">int</span> <span class="variable">registryId</span> <span class="operator">=</span> System.identityHashCode(registry);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.registriesPostProcessed.contains(registryId)) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(</span><br><span class="line">                <span class="string">&quot;postProcessBeanDefinitionRegistry already called on this post-processor against &quot;</span> + registry);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.factoriesPostProcessed.contains(registryId)) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(</span><br><span class="line">                <span class="string">&quot;postProcessBeanFactory already called on this post-processor against &quot;</span> + registry);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">this</span>.registriesPostProcessed.add(registryId);</span><br><span class="line"></span><br><span class="line">    processConfigBeanDefinitions(registry);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">processConfigBeanDefinitions</span><span class="params">(BeanDefinitionRegistry registry)</span> &#123;</span><br><span class="line">    List&lt;BeanDefinitionHolder&gt; configCandidates = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    String[] candidateNames = registry.getBeanDefinitionNames();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 遍历所有的bean definition的名字</span></span><br><span class="line">    <span class="keyword">for</span> (String beanName : candidateNames) &#123;</span><br><span class="line">        <span class="type">BeanDefinition</span> <span class="variable">beanDef</span> <span class="operator">=</span> registry.getBeanDefinition(beanName);</span><br><span class="line">        <span class="keyword">if</span> (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (logger.isDebugEnabled()) &#123;</span><br><span class="line">                logger.debug(<span class="string">&quot;Bean definition has already been processed as a configuration class: &quot;</span> + beanDef);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 检查这个bean definition是不是一个配置类</span></span><br><span class="line">        <span class="comment">// 1. 取得bean definition的metadata</span></span><br><span class="line">        <span class="comment">// 2. 设定configurationClass这个bean definition attribute的值，full或lite（暂不清楚是干什么用的）</span></span><br><span class="line">        <span class="comment">// 3. 如果指定了这个配置类的生效顺序，那么将其记录到order这个attribute中</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, <span class="built_in">this</span>.metadataReaderFactory)) &#123;</span><br><span class="line">            configCandidates.add(<span class="keyword">new</span> <span class="title class_">BeanDefinitionHolder</span>(beanDef, beanName));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Return immediately if no @Configuration classes were found</span></span><br><span class="line">    <span class="keyword">if</span> (configCandidates.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Sort by previously determined @Order value, if applicable</span></span><br><span class="line">    configCandidates.sort((bd1, bd2) -&gt; &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">i1</span> <span class="operator">=</span> ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());</span><br><span class="line">        <span class="type">int</span> <span class="variable">i2</span> <span class="operator">=</span> ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());</span><br><span class="line">        <span class="keyword">return</span> Integer.compare(i1, i2);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Detect any custom bean name generation strategy supplied through the enclosing application context</span></span><br><span class="line">    <span class="type">SingletonBeanRegistry</span> <span class="variable">sbr</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (registry <span class="keyword">instanceof</span> SingletonBeanRegistry _sbr) &#123;</span><br><span class="line">        sbr = _sbr;</span><br><span class="line">        <span class="keyword">if</span> (!<span class="built_in">this</span>.localBeanNameGeneratorSet) &#123;</span><br><span class="line">            <span class="type">BeanNameGenerator</span> <span class="variable">generator</span> <span class="operator">=</span> (BeanNameGenerator) sbr.getSingleton(</span><br><span class="line">                    AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);</span><br><span class="line">            <span class="keyword">if</span> (generator != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="built_in">this</span>.componentScanBeanNameGenerator = generator;</span><br><span class="line">                <span class="built_in">this</span>.importBeanNameGenerator = generator;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.environment == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="built_in">this</span>.environment = <span class="keyword">new</span> <span class="title class_">StandardEnvironment</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Parse each @Configuration class</span></span><br><span class="line">    <span class="type">ConfigurationClassParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConfigurationClassParser</span>(</span><br><span class="line">            <span class="built_in">this</span>.metadataReaderFactory, <span class="built_in">this</span>.problemReporter, <span class="built_in">this</span>.environment,</span><br><span class="line">            <span class="built_in">this</span>.resourceLoader, <span class="built_in">this</span>.componentScanBeanNameGenerator, registry);</span><br><span class="line"></span><br><span class="line">    Set&lt;BeanDefinitionHolder&gt; candidates = <span class="keyword">new</span> <span class="title class_">LinkedHashSet</span>&lt;&gt;(configCandidates);</span><br><span class="line">    Set&lt;ConfigurationClass&gt; alreadyParsed = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;(configCandidates.size());</span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        <span class="type">StartupStep</span> <span class="variable">processConfig</span> <span class="operator">=</span> <span class="built_in">this</span>.applicationStartup.start(<span class="string">&quot;spring.context.config-classes.parse&quot;</span>);</span><br><span class="line">        parser.parse(candidates);</span><br><span class="line">        parser.validate();</span><br><span class="line"></span><br><span class="line">        Set&lt;ConfigurationClass&gt; configClasses = <span class="keyword">new</span> <span class="title class_">LinkedHashSet</span>&lt;&gt;(parser.getConfigurationClasses());</span><br><span class="line">        configClasses.removeAll(alreadyParsed);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Read the model and create bean definitions based on its content</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.reader == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="built_in">this</span>.reader = <span class="keyword">new</span> <span class="title class_">ConfigurationClassBeanDefinitionReader</span>(</span><br><span class="line">                    registry, <span class="built_in">this</span>.sourceExtractor, <span class="built_in">this</span>.resourceLoader, <span class="built_in">this</span>.environment,</span><br><span class="line">                    <span class="built_in">this</span>.importBeanNameGenerator, parser.getImportRegistry());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">this</span>.reader.loadBeanDefinitions(configClasses);</span><br><span class="line">        alreadyParsed.addAll(configClasses);</span><br><span class="line">        processConfig.tag(<span class="string">&quot;classCount&quot;</span>, () -&gt; String.valueOf(configClasses.size())).end();</span><br><span class="line"></span><br><span class="line">        candidates.clear();</span><br><span class="line">        <span class="keyword">if</span> (registry.getBeanDefinitionCount() &gt; candidateNames.length) &#123;</span><br><span class="line">            String[] newCandidateNames = registry.getBeanDefinitionNames();</span><br><span class="line">            Set&lt;String&gt; oldCandidateNames = Set.of(candidateNames);</span><br><span class="line">            Set&lt;String&gt; alreadyParsedClasses = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;();</span><br><span class="line">            <span class="keyword">for</span> (ConfigurationClass configurationClass : alreadyParsed) &#123;</span><br><span class="line">                alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">for</span> (String candidateName : newCandidateNames) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!oldCandidateNames.contains(candidateName)) &#123;</span><br><span class="line">                    <span class="type">BeanDefinition</span> <span class="variable">bd</span> <span class="operator">=</span> registry.getBeanDefinition(candidateName);</span><br><span class="line">                    <span class="keyword">if</span> (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, <span class="built_in">this</span>.metadataReaderFactory) &amp;&amp;</span><br><span class="line">                            !alreadyParsedClasses.contains(bd.getBeanClassName())) &#123;</span><br><span class="line">                        candidates.add(<span class="keyword">new</span> <span class="title class_">BeanDefinitionHolder</span>(bd, candidateName));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            candidateNames = newCandidateNames;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">while</span> (!candidates.isEmpty());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes</span></span><br><span class="line">    <span class="keyword">if</span> (sbr != <span class="literal">null</span> &amp;&amp; !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) &#123;</span><br><span class="line">        sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary</span></span><br><span class="line">    <span class="built_in">this</span>.propertySourceDescriptors = parser.getPropertySourceDescriptors();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.metadataReaderFactory <span class="keyword">instanceof</span> CachingMetadataReaderFactory cachingMetadataReaderFactory) &#123;</span><br><span class="line">        <span class="comment">// Clear cache in externally provided MetadataReaderFactory; this is a no-op</span></span><br><span class="line">        <span class="comment">// for a shared cache since it&#x27;ll be cleared by the ApplicationContext.</span></span><br><span class="line">        cachingMetadataReaderFactory.clearCache();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先这里会遍历所有的bean definition，找到其中的配置类，然后调用<code>parse</code>方法解析它们。</p><figure class="highlight java"><table><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><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</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_">parse</span><span class="params">(Set&lt;BeanDefinitionHolder&gt; configCandidates)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (BeanDefinitionHolder holder : configCandidates) &#123;</span><br><span class="line">        <span class="type">BeanDefinition</span> <span class="variable">bd</span> <span class="operator">=</span> holder.getBeanDefinition();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 如果是从Spring Boot项目进来的，那么这里的bd就是主类封装成的AnnotatedGenericBeanDefinition</span></span><br><span class="line">            <span class="keyword">if</span> (bd <span class="keyword">instanceof</span> AnnotatedBeanDefinition annotatedBeanDef) &#123;</span><br><span class="line">                parse(annotatedBeanDef.getMetadata(), holder.getBeanName());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (bd <span class="keyword">instanceof</span> AbstractBeanDefinition abstractBeanDef &amp;&amp; abstractBeanDef.hasBeanClass()) &#123;</span><br><span class="line">                parse(abstractBeanDef.getBeanClass(), holder.getBeanName());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> &#123;</span><br><span class="line">                parse(bd.getBeanClassName(), holder.getBeanName());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">catch</span> (BeanDefinitionStoreException ex) &#123;</span><br><span class="line">            <span class="keyword">throw</span> ex;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">catch</span> (Throwable ex) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionStoreException</span>(</span><br><span class="line">                    <span class="string">&quot;Failed to parse configuration class [&quot;</span> + bd.getBeanClassName() + <span class="string">&quot;]&quot;</span>, ex);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 加载默认的配置，即自动装配的入口</span></span><br><span class="line">    <span class="built_in">this</span>.deferredImportSelectorHandler.process();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">parse</span><span class="params">(AnnotationMetadata metadata, String beanName)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    processConfigurationClass(<span class="keyword">new</span> <span class="title class_">ConfigurationClass</span>(metadata, beanName), DEFAULT_EXCLUSION_FILTER);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">processConfigurationClass</span><span class="params">(ConfigurationClass configClass, Predicate&lt;String&gt; filter)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">ConfigurationClass</span> <span class="variable">existingClass</span> <span class="operator">=</span> <span class="built_in">this</span>.configurationClasses.get(configClass);</span><br><span class="line">    <span class="keyword">if</span> (existingClass != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (configClass.isImported()) &#123;</span><br><span class="line">            <span class="keyword">if</span> (existingClass.isImported()) &#123;</span><br><span class="line">                existingClass.mergeImportedBy(configClass);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// Otherwise ignore new imported config class; existing non-imported class overrides it.</span></span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// Explicit bean definition found, probably replacing an import.</span></span><br><span class="line">            <span class="comment">// Let&#x27;s remove the old one and go with the new one.</span></span><br><span class="line">            <span class="built_in">this</span>.configurationClasses.remove(configClass);</span><br><span class="line">            <span class="built_in">this</span>.knownSuperclasses.values().removeIf(configClass::equals);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Recursively process the configuration class and its superclass hierarchy.</span></span><br><span class="line">    <span class="comment">// 递归处理当前的configuration class及其父类</span></span><br><span class="line">    <span class="type">SourceClass</span> <span class="variable">sourceClass</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line"></span><br><span class="line">        sourceClass = asSourceClass(configClass, filter);</span><br><span class="line">        <span class="keyword">do</span> &#123;</span><br><span class="line">            sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">while</span> (sourceClass != <span class="literal">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">catch</span> (IOException ex) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionStoreException</span>(</span><br><span class="line">                <span class="string">&quot;I/O failure while processing configuration class [&quot;</span> + sourceClass + <span class="string">&quot;]&quot;</span>, ex);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.configurationClasses.put(configClass, configClass);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ConfigurationClassParser</span></span><br><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> SourceClass <span class="title function_">doProcessConfigurationClass</span><span class="params">(</span></span><br><span class="line"><span class="params">        ConfigurationClass configClass, SourceClass sourceClass, Predicate&lt;String&gt; filter)</span></span><br><span class="line">        <span class="keyword">throws</span> IOException &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果这个configuration class带有@Component注解</span></span><br><span class="line">    <span class="comment">// 那么就递归处理其内部类</span></span><br><span class="line">    <span class="keyword">if</span> (configClass.getMetadata().isAnnotated(Component.class.getName())) &#123;</span><br><span class="line">        <span class="comment">// Recursively process any member (nested) classes first</span></span><br><span class="line">        processMemberClasses(configClass, sourceClass, filter);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process any @PropertySource annotations</span></span><br><span class="line">    <span class="comment">// 针对@PropertySource注解的处理</span></span><br><span class="line">    <span class="keyword">for</span> (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(</span><br><span class="line">            sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class,</span><br><span class="line">            PropertySources.class, <span class="literal">true</span>)) &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.propertySourceRegistry != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="built_in">this</span>.propertySourceRegistry.processPropertySource(propertySource);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            logger.info(<span class="string">&quot;Ignoring @PropertySource annotation on [&quot;</span> + sourceClass.getMetadata().getClassName() +</span><br><span class="line">                    <span class="string">&quot;]. Reason: Environment must implement ConfigurableEnvironment&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Search for locally declared @ComponentScan annotations first.</span></span><br><span class="line">    <span class="comment">// 根据@ComponentScan注解扫描项目中的bean</span></span><br><span class="line">    Set&lt;AnnotationAttributes&gt; componentScans = AnnotationConfigUtils.attributesForRepeatable(</span><br><span class="line">            sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class,</span><br><span class="line">            MergedAnnotation::isDirectlyPresent);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Fall back to searching for @ComponentScan meta-annotations (which indirectly</span></span><br><span class="line">    <span class="comment">// includes locally declared composed annotations).</span></span><br><span class="line">    <span class="comment">// 如果没找到直接使用的@ComponentScan注解，那么就尝试找间接的引用</span></span><br><span class="line">    <span class="comment">// 如 @SpringBootApplication -&gt; @ComponentScan</span></span><br><span class="line">    <span class="keyword">if</span> (componentScans.isEmpty()) &#123;</span><br><span class="line">        componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(),</span><br><span class="line">                ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!componentScans.isEmpty() &amp;&amp;</span><br><span class="line">            !<span class="built_in">this</span>.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) &#123;</span><br><span class="line">        <span class="keyword">for</span> (AnnotationAttributes componentScan : componentScans) &#123;</span><br><span class="line">            <span class="comment">// The config class is annotated with @ComponentScan -&gt; perform the scan immediately</span></span><br><span class="line">            <span class="comment">// 在这里立即开始扫描</span></span><br><span class="line">            Set&lt;BeanDefinitionHolder&gt; scannedBeanDefinitions =</span><br><span class="line">                    <span class="built_in">this</span>.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());</span><br><span class="line">            <span class="comment">// Check the set of scanned definitions for any further config classes and parse recursively if needed</span></span><br><span class="line">            <span class="keyword">for</span> (BeanDefinitionHolder holder : scannedBeanDefinitions) &#123;</span><br><span class="line">                <span class="type">BeanDefinition</span> <span class="variable">bdCand</span> <span class="operator">=</span> holder.getBeanDefinition().getOriginatingBeanDefinition();</span><br><span class="line">                <span class="keyword">if</span> (bdCand == <span class="literal">null</span>) &#123;</span><br><span class="line">                    bdCand = holder.getBeanDefinition();</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="comment">// 检查扫描到的这个bean是不是一个configuration class</span></span><br><span class="line">                <span class="comment">// 如果是，那么递归查找与它关脸的configuration class，如@Bean定义的bean，或带有@Import注解的@Component类</span></span><br><span class="line">                <span class="keyword">if</span> (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, <span class="built_in">this</span>.metadataReaderFactory)) &#123;</span><br><span class="line">                    parse(bdCand.getBeanClassName(), holder.getBeanName());</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process any @Import annotations</span></span><br><span class="line">    <span class="comment">// 递归处理@Import注解</span></span><br><span class="line">    processImports(configClass, sourceClass, getImports(sourceClass), filter, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process any @ImportResource annotations</span></span><br><span class="line">    <span class="type">AnnotationAttributes</span> <span class="variable">importResource</span> <span class="operator">=</span></span><br><span class="line">            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);</span><br><span class="line">    <span class="keyword">if</span> (importResource != <span class="literal">null</span>) &#123;</span><br><span class="line">        String[] resources = importResource.getStringArray(<span class="string">&quot;locations&quot;</span>);</span><br><span class="line">        Class&lt;? <span class="keyword">extends</span> <span class="title class_">BeanDefinitionReader</span>&gt; readerClass = importResource.getClass(<span class="string">&quot;reader&quot;</span>);</span><br><span class="line">        <span class="keyword">for</span> (String resource : resources) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">resolvedResource</span> <span class="operator">=</span> <span class="built_in">this</span>.environment.resolveRequiredPlaceholders(resource);</span><br><span class="line">            configClass.addImportedResource(resolvedResource, readerClass);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process individual @Bean methods</span></span><br><span class="line">    <span class="comment">// 处理带有@Bean的方法</span></span><br><span class="line">    Set&lt;MethodMetadata&gt; beanMethods = retrieveBeanMethodMetadata(sourceClass);</span><br><span class="line">    <span class="keyword">for</span> (MethodMetadata methodMetadata : beanMethods) &#123;</span><br><span class="line">        configClass.addBeanMethod(<span class="keyword">new</span> <span class="title class_">BeanMethod</span>(methodMetadata, configClass));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process default methods on interfaces</span></span><br><span class="line">    processInterfaces(configClass, sourceClass);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Process superclass, if any</span></span><br><span class="line">    <span class="keyword">if</span> (sourceClass.getMetadata().hasSuperClass()) &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">superclass</span> <span class="operator">=</span> sourceClass.getMetadata().getSuperClassName();</span><br><span class="line">        <span class="keyword">if</span> (superclass != <span class="literal">null</span> &amp;&amp; !superclass.startsWith(<span class="string">&quot;java&quot;</span>) &amp;&amp;</span><br><span class="line">                !<span class="built_in">this</span>.knownSuperclasses.containsKey(superclass)) &#123;</span><br><span class="line">            <span class="built_in">this</span>.knownSuperclasses.put(superclass, configClass);</span><br><span class="line">            <span class="comment">// Superclass found, return its annotation metadata and recurse</span></span><br><span class="line">            <span class="keyword">return</span> sourceClass.getSuperClass();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// No superclass -&gt; processing is complete</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Resource定位"><a href="#Resource定位" class="headerlink" title="Resource定位"></a>Resource定位</h3><p>这里的重点是<code>Set&lt;BeanDefinitionHolder&gt; scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());</code>这个方法调用，它负责处理扫描的具体过程，所以点进<code>parse</code>方法看它干了什么。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Set&lt;BeanDefinitionHolder&gt; <span class="title function_">parse</span><span class="params">(AnnotationAttributes componentScan, String declaringClass)</span> &#123;</span><br><span class="line">    <span class="comment">// 取得扫描器，并设置它的各个属性</span></span><br><span class="line">    <span class="type">ClassPathBeanDefinitionScanner</span> <span class="variable">scanner</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassPathBeanDefinitionScanner</span>(<span class="built_in">this</span>.registry,</span><br><span class="line">            componentScan.getBoolean(<span class="string">&quot;useDefaultFilters&quot;</span>), <span class="built_in">this</span>.environment, <span class="built_in">this</span>.resourceLoader);</span><br><span class="line"></span><br><span class="line">    Class&lt;? <span class="keyword">extends</span> <span class="title class_">BeanNameGenerator</span>&gt; generatorClass = componentScan.getClass(<span class="string">&quot;nameGenerator&quot;</span>);</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">useInheritedGenerator</span> <span class="operator">=</span> (BeanNameGenerator.class == generatorClass);</span><br><span class="line">    scanner.setBeanNameGenerator(useInheritedGenerator ? <span class="built_in">this</span>.beanNameGenerator :</span><br><span class="line">            BeanUtils.instantiateClass(generatorClass));</span><br><span class="line"></span><br><span class="line">    <span class="type">ScopedProxyMode</span> <span class="variable">scopedProxyMode</span> <span class="operator">=</span> componentScan.getEnum(<span class="string">&quot;scopedProxy&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (scopedProxyMode != ScopedProxyMode.DEFAULT) &#123;</span><br><span class="line">        scanner.setScopedProxyMode(scopedProxyMode);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        Class&lt;? <span class="keyword">extends</span> <span class="title class_">ScopeMetadataResolver</span>&gt; resolverClass = componentScan.getClass(<span class="string">&quot;scopeResolver&quot;</span>);</span><br><span class="line">        scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    scanner.setResourcePattern(componentScan.getString(<span class="string">&quot;resourcePattern&quot;</span>));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray(<span class="string">&quot;includeFilters&quot;</span>)) &#123;</span><br><span class="line">        List&lt;TypeFilter&gt; typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, <span class="built_in">this</span>.environment,</span><br><span class="line">                <span class="built_in">this</span>.resourceLoader, <span class="built_in">this</span>.registry);</span><br><span class="line">        <span class="keyword">for</span> (TypeFilter typeFilter : typeFilters) &#123;</span><br><span class="line">            scanner.addIncludeFilter(typeFilter);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray(<span class="string">&quot;excludeFilters&quot;</span>)) &#123;</span><br><span class="line">        List&lt;TypeFilter&gt; typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, <span class="built_in">this</span>.environment,</span><br><span class="line">            <span class="built_in">this</span>.resourceLoader, <span class="built_in">this</span>.registry);</span><br><span class="line">        <span class="keyword">for</span> (TypeFilter typeFilter : typeFilters) &#123;</span><br><span class="line">            scanner.addExcludeFilter(typeFilter);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">lazyInit</span> <span class="operator">=</span> componentScan.getBoolean(<span class="string">&quot;lazyInit&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (lazyInit) &#123;</span><br><span class="line">        scanner.getBeanDefinitionDefaults().setLazyInit(<span class="literal">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Set&lt;String&gt; basePackages = <span class="keyword">new</span> <span class="title class_">LinkedHashSet</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">// 取得basePackages的值，默认情况下为空，因为@SpringBootApplication里面就没给@ComponentScan配置这个值</span></span><br><span class="line">    String[] basePackagesArray = componentScan.getStringArray(<span class="string">&quot;basePackages&quot;</span>);</span><br><span class="line">    <span class="keyword">for</span> (String pkg : basePackagesArray) &#123;</span><br><span class="line">        String[] tokenized = StringUtils.tokenizeToStringArray(<span class="built_in">this</span>.environment.resolvePlaceholders(pkg),</span><br><span class="line">                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);</span><br><span class="line">        Collections.addAll(basePackages, tokenized);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 通过basePackageClasses里面指定的类找到它们所在的包，</span></span><br><span class="line">    <span class="comment">// 并将其加入basePackages</span></span><br><span class="line">    <span class="comment">// 默认情况下这个也是空的</span></span><br><span class="line">    <span class="keyword">for</span> (Class&lt;?&gt; clazz : componentScan.getClassArray(<span class="string">&quot;basePackageClasses&quot;</span>)) &#123;</span><br><span class="line">        basePackages.add(ClassUtils.getPackageName(clazz));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 查到最后basePackages还是空的话</span></span><br><span class="line">    <span class="comment">// 那就把带着@ComponentScan这个注解的类所在的包加到basePackages里面</span></span><br><span class="line">    <span class="comment">// 因为启动类带着这个注解，所以启动类所在的包就会被加进去</span></span><br><span class="line">    <span class="keyword">if</span> (basePackages.isEmpty()) &#123;</span><br><span class="line">        basePackages.add(ClassUtils.getPackageName(declaringClass));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    scanner.addExcludeFilter(<span class="keyword">new</span> <span class="title class_">AbstractTypeHierarchyTraversingFilter</span>(<span class="literal">false</span>, <span class="literal">false</span>) &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">protected</span> <span class="type">boolean</span> <span class="title function_">matchClassName</span><span class="params">(String className)</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> declaringClass.equals(className);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 根据basePackages扫描</span></span><br><span class="line">    <span class="keyword">return</span> scanner.doScan(StringUtils.toStringArray(basePackages));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>走到<code>scanner.doScan</code>的时候，就完成了定位这一步。接下来继续走进<code>ClassPathBeanDefinitionScanner#doScan</code>看它是怎么载入和注册各个BeanDefinition的。</p><figure class="highlight java"><table><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">protected</span> Set&lt;BeanDefinitionHolder&gt; <span class="title function_">doScan</span><span class="params">(String... basePackages)</span> &#123;</span><br><span class="line">    Assert.notEmpty(basePackages, <span class="string">&quot;At least one base package must be specified&quot;</span>);</span><br><span class="line">    Set&lt;BeanDefinitionHolder&gt; beanDefinitions = <span class="keyword">new</span> <span class="title class_">LinkedHashSet</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (String basePackage : basePackages) &#123;</span><br><span class="line">        <span class="comment">// 从指定的包中扫描需要装载的Bean</span></span><br><span class="line">        Set&lt;BeanDefinition&gt; candidates = findCandidateComponents(basePackage);</span><br><span class="line">        <span class="keyword">for</span> (BeanDefinition candidate : candidates) &#123;</span><br><span class="line">            <span class="comment">// 得到这个bean的scope，如singleton、prototype等</span></span><br><span class="line">            <span class="type">ScopeMetadata</span> <span class="variable">scopeMetadata</span> <span class="operator">=</span> <span class="built_in">this</span>.scopeMetadataResolver.resolveScopeMetadata(candidate);</span><br><span class="line">            <span class="comment">// 并将其设置到BeanDefinition中</span></span><br><span class="line">            candidate.setScope(scopeMetadata.getScopeName());</span><br><span class="line">            <span class="type">String</span> <span class="variable">beanName</span> <span class="operator">=</span> <span class="built_in">this</span>.beanNameGenerator.generateBeanName(candidate, <span class="built_in">this</span>.registry);</span><br><span class="line">            <span class="keyword">if</span> (candidate <span class="keyword">instanceof</span> AbstractBeanDefinition abstractBeanDefinition) &#123;</span><br><span class="line">                postProcessBeanDefinition(abstractBeanDefinition, beanName);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (candidate <span class="keyword">instanceof</span> AnnotatedBeanDefinition annotatedBeanDefinition) &#123;</span><br><span class="line">                AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 检查与这个bean名字对应的BeanDefinition是否已经被注册过</span></span><br><span class="line">            <span class="keyword">if</span> (checkCandidate(beanName, candidate)) &#123;</span><br><span class="line">                <span class="type">BeanDefinitionHolder</span> <span class="variable">definitionHolder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionHolder</span>(candidate, beanName);</span><br><span class="line">                definitionHolder =</span><br><span class="line">                        AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, <span class="built_in">this</span>.registry);</span><br><span class="line">                beanDefinitions.add(definitionHolder);</span><br><span class="line">                <span class="comment">// 将这个bean注册到IoC容器</span></span><br><span class="line">                registerBeanDefinition(definitionHolder, <span class="built_in">this</span>.registry);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> beanDefinitions;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="BeanDefinition载入"><a href="#BeanDefinition载入" class="headerlink" title="BeanDefinition载入"></a>BeanDefinition载入</h3><p>上面代码的for循环里首先会执行<code>findCandidateComponents</code>，这里就是负责载入这个basePackage下的所有BeanDefinition的。</p><figure class="highlight java"><table><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Set&lt;BeanDefinition&gt; <span class="title function_">findCandidateComponents</span><span class="params">(String basePackage)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">this</span>.componentsIndex != <span class="literal">null</span> &amp;&amp; indexSupportsIncludeFilters()) &#123;</span><br><span class="line">        <span class="keyword">return</span> addCandidateComponentsFromIndex(<span class="built_in">this</span>.componentsIndex, basePackage);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> scanCandidateComponents(basePackage);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> Set&lt;BeanDefinition&gt; <span class="title function_">scanCandidateComponents</span><span class="params">(String basePackage)</span> &#123;</span><br><span class="line">    Set&lt;BeanDefinition&gt; candidates = <span class="keyword">new</span> <span class="title class_">LinkedHashSet</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 根据basePackages生成classpath，</span></span><br><span class="line">        <span class="comment">// 如classpath*:com/example/demo/**/*.class</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">packageSearchPath</span> <span class="operator">=</span> ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +</span><br><span class="line">                resolveBasePackage(basePackage) + <span class="string">&#x27;/&#x27;</span> + <span class="built_in">this</span>.resourcePattern;</span><br><span class="line">        <span class="comment">// 从这个classpath下面扫描所有匹配的资源，也就是类</span></span><br><span class="line">        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">traceEnabled</span> <span class="operator">=</span> logger.isTraceEnabled();</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">debugEnabled</span> <span class="operator">=</span> logger.isDebugEnabled();</span><br><span class="line">        <span class="comment">// 循环处理上面找到的类，将符合条件的类包装成ScannedGenericBeanDefinition并加到candidates中</span></span><br><span class="line">        <span class="keyword">for</span> (Resource resource : resources) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">filename</span> <span class="operator">=</span> resource.getFilename();</span><br><span class="line">            <span class="keyword">if</span> (filename != <span class="literal">null</span> &amp;&amp; filename.contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) &#123;</span><br><span class="line">                <span class="comment">// Ignore CGLIB-generated classes in the classpath</span></span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (traceEnabled) &#123;</span><br><span class="line">                logger.trace(<span class="string">&quot;Scanning &quot;</span> + resource);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="type">MetadataReader</span> <span class="variable">metadataReader</span> <span class="operator">=</span> getMetadataReaderFactory().getMetadataReader(resource);</span><br><span class="line">                <span class="comment">// 检查这个类是不是既没有命中任何一个exclude filter，并至少命中了一个include filter</span></span><br><span class="line">                <span class="comment">// 其中一个include filter就是根据@Component注解来过滤的</span></span><br><span class="line">                <span class="keyword">if</span> (isCandidateComponent(metadataReader)) &#123;</span><br><span class="line">                    <span class="type">ScannedGenericBeanDefinition</span> <span class="variable">sbd</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ScannedGenericBeanDefinition</span>(metadataReader);</span><br><span class="line">                    sbd.setSource(resource);</span><br><span class="line">                    <span class="keyword">if</span> (isCandidateComponent(sbd)) &#123;</span><br><span class="line">                        <span class="comment">// 打日志，略</span></span><br><span class="line">                        candidates.add(sbd);</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="comment">// 打日志，略</span></span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// 打日志，略</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">catch</span> (FileNotFoundException ex) &#123;</span><br><span class="line">                <span class="comment">// 打日志，略</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">catch</span> (ClassFormatException ex) &#123;</span><br><span class="line">                <span class="keyword">if</span> (shouldIgnoreClassFormatException) &#123;</span><br><span class="line">                    <span class="comment">// 打日志，略</span></span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionStoreException</span>(<span class="string">&quot;Incompatible class format in &quot;</span> + resource +</span><br><span class="line">                            <span class="string">&quot;: set system property &#x27;spring.classformat.ignore&#x27; to &#x27;true&#x27; &quot;</span> +</span><br><span class="line">                            <span class="string">&quot;if you mean to ignore such files during classpath scanning&quot;</span>, ex);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">catch</span> (Throwable ex) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionStoreException</span>(<span class="string">&quot;Failed to read candidate component class: &quot;</span> + resource, ex);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">catch</span> (IOException ex) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanDefinitionStoreException</span>(<span class="string">&quot;I/O failure during classpath scanning&quot;</span>, ex);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> candidates;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="注册BeanDefinition"><a href="#注册BeanDefinition" class="headerlink" title="注册BeanDefinition"></a>注册BeanDefinition</h3><p>回到<code>doScan</code>方法，在得到所有的BeanDefinition之后，就会调用<code>registerBeanDefinition</code>方法来完成BeanDefinition的注册。</p><figure class="highlight java"><table><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"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">registerBeanDefinition</span><span class="params">(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)</span> &#123;</span><br><span class="line">    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// BeanDefinitionReaderUtils</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_">registerBeanDefinition</span><span class="params">(</span></span><br><span class="line"><span class="params">        BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)</span></span><br><span class="line">        <span class="keyword">throws</span> BeanDefinitionStoreException &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Register bean definition under primary name.</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">beanName</span> <span class="operator">=</span> definitionHolder.getBeanName();</span><br><span class="line">    <span class="comment">// 将这个bean注册到DefaultListableBeanFactory的BeanDefinitionMap</span></span><br><span class="line">    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Register aliases for bean name, if any.</span></span><br><span class="line">    String[] aliases = definitionHolder.getAliases();</span><br><span class="line">    <span class="keyword">if</span> (aliases != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (String alias : aliases) &#123;</span><br><span class="line">            registry.registerAlias(beanName, alias);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就是把传进来的BeanDefinition注册到<code>DefaultListableBeanFactory</code>的<code>BeanDefinitionMap</code>中。这部分在上一篇博文中已经提过，就不重复了。</p><p>在<code>doScan</code>方法的for循环遍历完所有的BeanDefinition之后，所有带有<code>@Component</code>注解的bean就全部注册好了。接下来会回到<code>doProcessConfigurationClass</code>方法，继续处理带有<code>@Import</code>注解的类和带有<code>@Bean</code>注解的方法。</p><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><ul><li><span class="exturl" data-url="aHR0cHM6Ly93d3cuY25ibG9ncy5jb20vaGVsbG8tc2hmL3AvMTEwNTE0NzYuaHRtbA==">SpringBoot启动流程分析（四）：IoC容器的初始化过程<i class="fa fa-external-link-alt"></i></span></li></ul>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
