<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="https://levy.vip/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <atom:link href="https://levy.vip/rss.xml" rel="self" type="application/rss+xml"/>
    <title>levy</title>
    <link>https://levy.vip/</link>
    <description>levy&amp;apos;s blog</description>
    <language>zh-CN</language>
    <pubDate>Tue, 28 May 2024 23:24:31 GMT</pubDate>
    <lastBuildDate>Tue, 28 May 2024 23:24:31 GMT</lastBuildDate>
    <generator>vuepress-plugin-feed2</generator>
    <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
    <item>
      <title>VuePress2 娱乐视频</title>
      <link>https://levy.vip/daily/a-vuepress2-entertaining-video.html</link>
      <guid>https://levy.vip/daily/a-vuepress2-entertaining-video.html</guid>
      <source url="https://levy.vip/rss.xml">VuePress2 娱乐视频</source>
      <description>VuePress2 娱乐视频 参考《原神，启动》的梗，做的一个娱乐向视频。</description>
      <pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> VuePress2 娱乐视频</h1>
<p>参考《原神，启动》的梗，做的一个娱乐向视频。</p>
]]></content:encoded>
    </item>
    <item>
      <title>来自Navicat的侵权警告</title>
      <link>https://levy.vip/daily/a-warning-from-navicat.html</link>
      <guid>https://levy.vip/daily/a-warning-from-navicat.html</guid>
      <source url="https://levy.vip/rss.xml">来自Navicat的侵权警告</source>
      <description>来自Navicat的侵权警告 公司收到了Navicat的侵权警告, 很有可能要吃官司。在此还是呼吁大家使用正版，拒绝使用盗版软件。 另外，开发者常用的软件的合法替代品，视频中也有推荐。</description>
      <pubDate>Mon, 31 Jul 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 来自Navicat的侵权警告</h1>
<p>公司收到了Navicat的侵权警告, 很有可能要吃官司。在此还是呼吁大家使用正版，拒绝使用盗版软件。</p>
<p>另外，开发者常用的软件的合法替代品，视频中也有推荐。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Beyond UTF-8, do you know utf8mb4 and utf8mb4_unicode_ci?</title>
      <link>https://levy.vip/daily/beyond-utf8-do-you-know-utf8mb4-and-collation.html</link>
      <guid>https://levy.vip/daily/beyond-utf8-do-you-know-utf8mb4-and-collation.html</guid>
      <source url="https://levy.vip/rss.xml">Beyond UTF-8, do you know utf8mb4 and utf8mb4_unicode_ci?</source>
      <description>Beyond UTF-8, do you know utf8mb4 and utf8mb4_unicode_ci? Background Look at the DDL below, can you tell the meaning of CHARSET=utf8mb4 and COLLATE=utf8mb4_general_ci? CREATE TABLE `my_table` ( `id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; That is the knowledge that today I want to share with you.</description>
      <pubDate>Tue, 15 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Beyond UTF-8, do you know utf8mb4 and utf8mb4_unicode_ci?</h1>
<h2> Background</h2>
<p>Look at the DDL below, can you tell the meaning of <code>CHARSET=utf8mb4</code> and <code>COLLATE=utf8mb4_general_ci</code>?</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>That is the knowledge that today I want to share with you.</p>
<!-- more -->
]]></content:encoded>
    </item>
    <item>
      <title>Claude AI应用案例，从HTML中抽取文本</title>
      <link>https://levy.vip/daily/claude-ai-in-action-extract-info-from-html.html</link>
      <guid>https://levy.vip/daily/claude-ai-in-action-extract-info-from-html.html</guid>
      <source url="https://levy.vip/rss.xml">Claude AI应用案例，从HTML中抽取文本</source>
      <description>Claude AI应用案例，从HTML中抽取文本 今天跟大家分享AI使用心得：不再是让AI帮我们写代码，而是直接帮我们干活！</description>
      <pubDate>Fri, 25 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Claude AI应用案例，从HTML中抽取文本</h1>
<p>今天跟大家分享AI使用心得：不再是让AI帮我们写代码，而是直接帮我们干活！</p>
<!-- more -->
]]></content:encoded>
    </item>
    <item>
      <title>复制代码也许不是罪</title>
      <link>https://levy.vip/daily/copy-code-may-not-be-guilty.html</link>
      <guid>https://levy.vip/daily/copy-code-may-not-be-guilty.html</guid>
      <source url="https://levy.vip/rss.xml">复制代码也许不是罪</source>
      <description>复制代码也许不是罪 前言 熟悉我的人都知道，我对代码是有追求的。 正式参加工作后，我就知道，复制粘贴是坏的实践，自己一直极力避免做这样的事。要是遇到了别人复制粘贴，要么喷，要么自己改。 我早期认为：复制代码就是菜。 后来认为：复制代码可能不是菜，而是懒，没有素养，自我要求。 而现在：代码其实也没那么重要；某些情况下复制粘贴是可以接受的。 编码经过七个年头，我思想上为何会有如此改变？难道这就是传说中的七年之痒?</description>
      <pubDate>Sun, 24 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 复制代码也许不是罪</h1>
<h2> 前言</h2>
<p>熟悉我的人都知道，我对代码是有追求的。</p>
<p>正式参加工作后，我就知道，复制粘贴是坏的实践，自己一直极力避免做这样的事。要是遇到了别人复制粘贴，要么喷，要么自己改。</p>
<p>我早期认为：复制代码就是菜。</p>
<p>后来认为：复制代码可能不是菜，而是懒，没有素养，自我要求。</p>
<p>而现在：代码其实也没那么重要；某些情况下复制粘贴是可以接受的。</p>
<p>编码经过七个年头，我思想上为何会有如此改变？难道这就是传说中的七年之痒?</p>
<!-- more -->
<h2> 正文</h2>
<p>我总结了一下，主要有以下原因：<br>
1.深刻地理解了源码与测试用例之间的关系。只相信测试用例，而不是相信源码。如果你只改源码而不是补充相应的测试代码，那所谓的有追求是盲目、片面的。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128677-28080825-d512-41fe-85e4-6a56553d25f1.jpeg" alt=""></p>
<p>2.对具体的编码实现已经缺乏兴致了。尤其是有AI的情况下，真的是有手就会，何必要我来写呢？我负责设计，再进行 Code Review　不是更好吗？<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-09-24-IMG_2577.jpg" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-09-24-IMG_2578.jpg" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-09-24-IMG_2579.jpg" alt=""></p>
<p>3.要考虑时间成本。如果别人已经做了一大半，我何必重头开始？</p>
<p>4.提升对质量的认知。聚集于产出质量，而不仅仅是代码质量，那么你就会发现，要做的事还挺多，再考虑时间成本，需要做一个权衡取舍。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539182620-f3e4d2e3-bd24-4211-bb61-f5104b0e7ef3.jpeg" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539182844-04210738-e753-43c8-8917-a1c98e8f4d77.jpeg" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128677-28080825-d512-41fe-85e4-6a56553d25f1.jpeg" type="image/jpeg"/>
    </item>
    <item>
      <title>不要与傻逼进行争吵</title>
      <link>https://levy.vip/daily/dont-try-to-argue-with-a-sb.html</link>
      <guid>https://levy.vip/daily/dont-try-to-argue-with-a-sb.html</guid>
      <source url="https://levy.vip/rss.xml">不要与傻逼进行争吵</source>
      <description>不要与傻逼进行争吵 这是个职场吐槽帖，也是个自我反思、情绪管理帖。 我在沟通上，有以下可以改正的点： 0. 这个东西我也不太懂，要不你先自己看一下。改成：我帮你问一下，等下搞清楚了回复你 我要下班了，周一再回。改成：我身体不舒服，先回去了，等下回复你 你自己安排不合理，凭什么要我陪你加班？改成：放心，我配合你，有事就沟通 你看不懂代码吗？怎么当P7的？改成：你还需要什么，你说；你还有哪里不懂的，我给你解答 最后，全程忍住反问的冲动，不要使用反问句，尽量改成陈述句。陈述句使人冷静，反问句只会让矛盾升级，加剧愤怒！</description>
      <pubDate>Fri, 04 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 不要与傻逼进行争吵</h1>
<p>这是个职场吐槽帖，也是个自我反思、情绪管理帖。</p>
<p>我在沟通上，有以下可以改正的点：<br>
0. 这个东西我也不太懂，要不你先自己看一下。改成：我帮你问一下，等下搞清楚了回复你</p>
<ol>
<li>我要下班了，周一再回。改成：我身体不舒服，先回去了，等下回复你</li>
<li>你自己安排不合理，凭什么要我陪你加班？改成：放心，我配合你，有事就沟通</li>
<li>你看不懂代码吗？怎么当P7的？改成：你还需要什么，你说；你还有哪里不懂的，我给你解答</li>
<li>最后，全程忍住反问的冲动，不要使用反问句，尽量改成陈述句。陈述句使人冷静，反问句只会让矛盾升级，加剧愤怒！</li>
</ol>
<!-- more -->
<p>事情是这样的。</p>
<p>我跟素未谋面的同事在工作上要有一些配合，主要是我提供技术方案、文档给他。</p>
<p>对方工作能力不足就算了，时间管理也不行。8.3号一整天没来找我，8.4号白天也没来找我，快下班了，才打电话跟我说要资料。</p>
<p>这是第一层不满：快下班了，没什么耐心了，我不想别人打扰我周五下班。</p>
<p>电话中，他的语气带着责备，明显是甩锅的意味。找人办事，要别人配合还用这种语气，这是第二层不满。我开始怒他，我说，“兄弟，你冷静下。”</p>
<p>接着，他又跟我扯淡，我一看时间，六点钟了，是时候下班。我就说，“有什么问题发钉钉，我周一给你解决。”　他急了，他说，“你不能下班，你不能走，你要陪我！”<br>
tmd，自己延误工期，还要我陪你加班，哪个这个道理？这是第三层不满，我有点破防了。我开始说一些不太理智、不太专业的话。</p>
<p>我说，“凭什么要我陪你加班？V我50，我就陪你”。他说，“你在搞笑吧，不如让领导V你50？”　<br>
他接着说，“这个东西很急，客户今天晚上就要！”　我说，“那是你的事，你的客户，又不是我的客户。”</p>
<p>这两句话一出，味道就有点不对了。本来全然是他不占理的，现在他抓住的我语言上漏洞，开始要拉我下水了。</p>
<p>于是马上拉群、打字，向领导打小报告，一幅小学生向老师告状的模样。</p>
<p>这情商，这行为，我们还能好好配合？我们接下来净在群里互怒，要么是甩锅，要么是暗示对方能力不行，没有一个人在解决问题。</p>
<p>最后，我回去以后，还是带着不满加了班。不但自己加，还搞得我的 leader　也跟着加，让　leader 对那个人也无语，心情不好了。</p>
<p>这一点，我有点自责，因为我连累了我的 leader。而这也是我为什么反思并把这件事情写出来的原因，自己不爽只是自己的事，连累到他人，我就过意不去了。</p>
<p>我不希望这种事情再次发生！我应该能掌握事态，Let me take control of things!</p>
<p>下次再有这种事，一定是先把工作上的问题解决，然后再远离傻逼。不要在傻逼身上浪费时间，多说一句话、甚至只是骂一句都不值得！</p>
<p>想一想，已经几年没跟人因为工作的事情跟人发生明面上的不愉快了。我的情绪管理能力似乎下降了，这也是个契机，让我重新对这方面重视起来。</p>
<p>经验上看，除了开头的句式转换以外，其实有一个心法值得学习。那就是<strong>永远不要跟人在明面上闹不愉快</strong>。</p>
<p>同时，结合“阳奉阴违”的技巧，你不用去做口舌之争，你去做你的事就行了。<br>
比如，我准备下班了，对方说，“你不要下班，你不能走。”　我可以回复，“好的，我不走，我上个厕所。”　然后我就挂掉电话，下班走人，这不是很简单吗？何必反问他“为什么我不能走？”没有意义啊！</p>
<p>总之，明面上不与人闹翻，保持自己情绪的稳定，一方面是职业素养体现，另一方面也是自己涵养的体现。<br>
还有一方面，那就是有利于身心健康——生气是会对身体器官造成伤害的，如高血压、胃疼、肠胃不适！</p>
]]></content:encoded>
    </item>
    <item>
      <title>迭代复盘之三员管理</title>
      <link>https://levy.vip/daily/iteration-retrospective-of-sanyuan.html</link>
      <guid>https://levy.vip/daily/iteration-retrospective-of-sanyuan.html</guid>
      <source url="https://levy.vip/rss.xml">迭代复盘之三员管理</source>
      <description>迭代复盘之三员管理 前言 本次迭代做的工作主要是回收项目能力，具体做法是把 fork 出去的代码合并回来。 这次迭代因为各种原因，延期了快一个星期（周六还加了班）。 那么，我从工作流、方法论的角度，反思了自己可以改进的点，期望在这种迁移旧代码的实践中，抽取出能复用的经验。</description>
      <pubDate>Fri, 08 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 迭代复盘之三员管理</h1>
<h2> 前言</h2>
<p>本次迭代做的工作主要是回收项目能力，具体做法是把 fork 出去的代码合并回来。</p>
<p>这次迭代因为各种原因，延期了快一个星期（周六还加了班）。</p>
<p>那么，我从工作流、方法论的角度，反思了自己可以改进的点，期望在这种迁移旧代码的实践中，抽取出能复用的经验。</p>
<!-- more -->
<h2> 动手前，至少梳理出接口清单</h2>
<p>太轻信旧代码了，抱着代码迁移过来，跑起来就能用的态度，没能及时发现功能遗漏点，到了提测才发现有功能未实现，这是可以避免的。</p>
<p>更优的工作流应该是：</p>
<ol>
<li>先进行业务梳理</li>
<li>整理出接口清单</li>
<li>再进行代码迁移</li>
<li>根据接口清单，逐个验证迁移的代码是否符合需求</li>
</ol>
<h2> 迁移时，采取结队编程</h2>
<p>看到有 500 个文件要迁移，觉得用分治的思路去实现会更高效，但这种思维有一个误区：做得快不等于有效率，因为效率需要的是有用功，而不是无用功。</p>
<p>实践表明，初期对业务的理解、代码的熟悉程度还不够深入，迁移代码遇到分歧点时，个人在独自处理时容易产生错误——而这种错误并不自知，这才是最蛋疼的地方。</p>
<p>所以，我才建议，采取结队编程实践，两个人一起来迁移代码，这样子遇到分歧时可以讨论，而不是贸然行动。表面上看，这样的进程慢了，实际上却能提高对代码、业务的理解，提高了迁移的正确率，真正地提高效率。</p>
<h2> 迁移后，需要对自己负责的功能设计测试用例</h2>
<p>这里的本质，是不能完全依赖别人。虽然测试人员会编写测试用例，也会进行用例评审，但这里会有两个问题：</p>
<ol>
<li>前期评审时，自己的心思可能在理解业务、功能设计与改造以及代码迁移，没有多少心思能去 debug 用例的精细程度</li>
<li>测试也是人，也会有遗漏，并且没有接触源码，有些特殊的、极端的边界场景，自己作为研发人员，更容易发现</li>
</ol>
<p>可以看成，我们自己设计的用例是对测试人员给出的测试用例的补充。</p>
<p>当然，由于时间的关系，做完善的测试一般都不太可能了。<br>
对于 legacy code，想补充 service 层的单元测试成本是很高的。写 controller 层的测试，可能也没有太多时间。<br>
但至少应该用 postman 进行调试，或者在用户界面上点几下，不能完全等着前端或测试人员反馈，认为没有反馈就是没有 bug。</p>
]]></content:encoded>
    </item>
    <item>
      <title>都什么年代了，还在用传统方式写代码？</title>
      <link>https://levy.vip/daily/leverage-ai-to-boost-coding-productivity.html</link>
      <guid>https://levy.vip/daily/leverage-ai-to-boost-coding-productivity.html</guid>
      <source url="https://levy.vip/rss.xml">都什么年代了，还在用传统方式写代码？</source>
      <description>都什么年代了，还在用传统方式写代码？ 前言 还在把 AI 当作搜索引擎的替代品，有问题才问 AI，没问题就在那边吭哧吭哧地撸代码？如果是这样，那你真的 OUT了！现在正经人谁还自己一行行地写代码啊，都是 AI 生成的代码——没有 AI 我不写（手动滑稽）。 本文将搁置争议，秉持实用主义，讨论在 AI 可以辅助我们编码的情况下，应采取什么样的实践，从而利用好工具，提高工作效率。 本文将分享 AI 时代的编程新实践，教你如何从一个 &amp;quot;Ctrl + C&amp;quot;、 &amp;quot;Ctrl + V&amp;quot; 工程师，变成一个 &amp;quot;Tab + Enter&amp;quot; 工程师🤣。</description>
      <pubDate>Sat, 26 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 都什么年代了，还在用传统方式写代码？</h1>
<h2> 前言</h2>
<p>还在把 AI 当作搜索引擎的替代品，有问题才问 AI，没问题就在那边吭哧吭哧地撸代码？如果是这样，那你真的 OUT了！现在正经人谁还自己一行行地写代码啊，都是 AI 生成的代码——没有 AI 我不写（手动滑稽）。</p>
<p>本文将搁置争议，秉持实用主义，讨论在 AI 可以辅助我们编码的情况下，应采取什么样的实践，从而利用好工具，提高工作效率。</p>
<p>本文将分享 AI 时代的编程新实践，教你如何从一个 "Ctrl + C"、 "Ctrl + V" 工程师，变成一个  "Tab + Enter" 工程师🤣。</p>
<h2> 开发流程</h2>
<p>软件的一般研发流程为：</p>
<ol>
<li>需求分析</li>
<li>程序设计</li>
<li>代码编写</li>
<li>软件测试</li>
<li>部署上线</li>
</ol>
<p>我们在这里主要关心步骤2~4，因为与 AI 结合得比较紧密。虽然需求分析也可以借助 AI，但不是本文的重点，故不做讨论。</p>
<h2> 程序设计</h2>
<p>经过需求分析、逻辑梳理后，在编写实际代码前，需要进行程序设计。</p>
<p>此环节的产物是设计文档，是什么类型的设计文档不重要，重要的是伪代码的输出。</p>
<p>虽然《Code Complete》早就推荐过伪代码的实践，但对此人们容易有一个误区：认为写伪代码花的时间，已经够把实际代码写好了。但 AIGC 时代，此问题可以轻松破解：AI 写代码的速度肯定比人快，因此，只要能找到方法能让 AI 生成符合需求的代码，就值得花时间去研究。而伪代码，就是让 AI 快速生成符合期望的实际代码的最好方式。</p>
<p>为什么这么说呢？因为想要让 AIGC 符合期望，恰当的 Prompt 必不可少。但如何写好这个 Prompt，需要提供多少上下文，才能让 AI 更好地理解我们的意图，这是需要技巧、需要调试的。而经过精心设计的伪代码，本身已经提供了足够的上下文，且意图足够聚焦，减轻了对 Prompt 的要求，又提高了 AIGC 的成功率。</p>
<p>我们来看一下伪代码示例：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>上述代码主要表达的内容是：</p>
<ol>
<li>对初始数据进行加密</li>
<li>返回加密后的相关内容</li>
<li>涉及到了一系列的算法</li>
</ol>
<p>对于伪代码的编写，有如以建议：</p>
<ol>
<li>不要纠结类型，不要局限于某一种编程语言的语法，不用试图写能编译通过的代码——这只会限制了你的表达</li>
<li>命名一定要准确，符合领域术语，这一点很关键。这一点可以通过查找资料、看书、问 AI 来实现。千万不要只是生硬地汉译英，不专业的表达会妨碍 AI 的理解</li>
</ol>
<h2> 代码编写</h2>
<p>代码编写分为以下几步：</p>
<ol>
<li>把伪代码转换成目标语言可执行的代码</li>
<li>根据项目情况，对生成的代码进行改造</li>
<li>利用 AI 编码辅助工具编写剩余的代码</li>
</ol>
<h3> 生成真实代码</h3>
<p>让 AI 生成代码的 prompt 很简单，示例如下：</p>
<div class="language-text line-numbers-mode" data-ext="text"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>根据实际情况，把 Java 替换成 Node.js/Python/Go 即可。</p>
<p>使用 chatGPT 结果截图：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689239437556-36a14986-6114-44e1-b07c-c38a288874db.png" alt=""></p>
<p>完整代码如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>把上述代码 copy 下来，放到工程中，根据需要改造即可。</p>
<p>这里特别要说下，强烈推荐使用原版 AI，而不是寻找平替，同样的 prompt，下图是某一中文平替输出的结果：<br>
只生成了函数声明，没有生成函数实现。二者对比，未免相形见绌。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688721631064-514593cc-21d3-4851-8f41-71e776658295.png" alt=""></p>
<h3> 辅助编程工具</h3>
<p>改造的过程中，少不了 AI pair programming tools。对此，我推荐使用 Amazon 的 CodeWhisperer，原因很简单，跟 GitHub Copilot 相比，它是免费的😃。</p>
<p>CodeWhisperer 的安装可以看文末的安装教程，我们先来看一下它是怎么辅助我们编码的。</p>
<p>第一种方式是最简单的，那就是什么都不管，等待智能提示即可，就好像 IDEA 原来的提示一样，只不过更智能。</p>
<p>下图示例中，要把原来的中文异常提示，修改成英文，而我只输入了两个字符 <code>IM</code>， 就得到了智能提示，补全了完整的英文字符串！<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689230327179-e9bf9c63-7081-41ca-8720-840f0f7f7c77.png" alt=""><br>
可以注意到，上图的智能建议一共有 5 条，相应的快捷键为：</p>
<ol>
<li>方向键 -&gt;，查看下一条提示</li>
<li>方向键 &lt;-，查看上一条提示</li>
<li>Tab，采用该提示</li>
<li>Esc，拒绝提示</li>
</ol>
<p>我们再来看第二种 CodeWhisperer 的使用方式，编写注释，获得编码建议。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689301926714-26dc940a-9762-4b3b-ab3d-f59cff96d54d.gif#averageHue=%23436733&amp;clientId=u2b1600a8-64be-4&amp;from=paste&amp;height=720&amp;id=u7ba54448&amp;originHeight=720&amp;originWidth=1280&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=663291&amp;status=done&amp;style=none&amp;taskId=ufeb3c36e-8e32-419e-bbe3-8acd6f88063&amp;title=&amp;width=1280" alt=""><br>
最后一种就是编写一个空函数，让 CodeWhisperer 根据函数名去猜测函数的实现，这种情况需要足够的上下文，才能得到令人满意的结果。</p>
<h2> 软件测试</h2>
<p>AI 生成的内容，并不是完全可信任的，因此，单元测试的重要性变得尤为突出。</p>
<p>对上述代码编写测试代码后，实际上并不能一次通过，因为前面 AI 生成的代码参数有误。</p>
<p>此时需要一边执行单测，一边根据结果与 AI 进行交互：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689240199090-8daed9cd-273d-406d-9f20-60ba66732436.png" alt=""></p>
<p>经过修改，最终测试用例通过👏！<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689240017776-4f52a685-d749-4af3-8061-94c4befe3e8b.png" alt=""></p>
<h2> 总结</h2>
<p>本文通过案例，展示了 AI 如何结合软件研发的流程，提升我们的编程效率的。</p>
<p>其中，个人认为最重要的是编写伪代码与进行单元测试。有趣的是，这两样实践在 AIGC 时代之前，就已经被认为是最佳实践。这给我们启示：某些方法论、实践经得起时间的考验，技术更新迭代，它们历久弥新。</p>
<p>对于AI编程的到底应该怎么看，其实 GitHub Copilot 的产品标语已经很好地概括了：Your AI pair programmer。在结队编程里，由两个人一起来进行编程活动。一个是 Navigator，负责指导、审查，另一个 Driver，负责具体的代码编写。AI 的出现，其实是取代了 Driver 的角色，而另一个充当 Navigator 角色的人，仍然是不可或缺的。</p>
<p>最后，AI 是否能进一步渗透我们的工作流，还有待探索。此文作引抛砖引玉之用，期待大家的后续分享。</p>
<h2> 附：CodeWhisperer 安装</h2>
<p>下载 2023 年的 IDEA，打开 Plugins Marketplace，找到 AWS Toolkit<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688371554642-c3105125-ac08-4d22-8da5-c6cec8b898fa.png" alt=""></p>
<p>安装完成、重启 IDEA 后，点击左下角，按下图所示操作：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688371684736-b48a0257-1aa1-442f-b998-05aa79bf0863.png" alt=""></p>
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688371750789-6aabc4d0-512e-44bc-9c12-13d02c9f8527.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>如果第一次使用，就点击 1 处进行注册，如果已经有账号了，就点击 2 处使用自己的账号登录。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688371861568-6da13369-3074-4f4e-86e6-0a2c8738b30c.png" alt=""><br>
注册、登录、授权成功后，出现如图所示页面，即可使用 CodeWhisperer。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1688371897998-369e984e-f5b8-4c4b-bc56-897015092549.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1689239437556-36a14986-6114-44e1-b07c-c38a288874db.png" type="image/png"/>
    </item>
    <item>
      <title>微软中国CTO演讲观后感</title>
      <link>https://levy.vip/daily/reflections-on-a-speech-by-cto-of-microsoft-china.html</link>
      <guid>https://levy.vip/daily/reflections-on-a-speech-by-cto-of-microsoft-china.html</guid>
      <source url="https://levy.vip/rss.xml">微软中国CTO演讲观后感</source>
      <description>微软中国CTO演讲观后感 看看大佬的演讲，还是有很多启发的。别的不说，推荐的书单就很有价值。</description>
      <pubDate>Sun, 10 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 微软中国CTO演讲观后感</h1>
<p>看看大佬的演讲，还是有很多启发的。别的不说，推荐的书单就很有价值。</p>
<!-- more -->
]]></content:encoded>
    </item>
    <item>
      <title>生产教训：测试环境要与生产环境一致</title>
      <link>https://levy.vip/daily/testing-environments-should-be-consistent-with-production-environments.html</link>
      <guid>https://levy.vip/daily/testing-environments-should-be-consistent-with-production-environments.html</guid>
      <source url="https://levy.vip/rss.xml">生产教训：测试环境要与生产环境一致</source>
      <description>生产教训：测试环境要与生产环境一致 事件还原 业务流程： app-a 上传文件 app-b 下载文件后使用文件 其他信息： 开发、测试环境使用 MinIO 生产环境使用 Amazon S3 问题： app-a 上传文件成功 app-b 使用文件报错 逐步分析定位问题： app-a 与 app-b　配置是否一致？——确认都是使用 S3 S3 是否正确配置？有没权限问题？——确认配置正确，没有权限问题 app-a 是否真的上传成功？——确认文件已在 S3 app-b 是否下载成功？——根据日志，判断下载失败，得到的信息是：文件不存在。</description>
      <pubDate>Sun, 12 Nov 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 生产教训：测试环境要与生产环境一致</h1>
<h2> 事件还原</h2>
<p>业务流程：</p>
<ol>
<li>app-a 上传文件</li>
<li>app-b 下载文件后使用文件</li>
</ol>
<p>其他信息：</p>
<ol>
<li>开发、测试环境使用 MinIO</li>
<li>生产环境使用 Amazon S3</li>
</ol>
<p>问题：</p>
<ol>
<li>app-a 上传文件成功</li>
<li>app-b 使用文件报错</li>
</ol>
<p>逐步分析定位问题：</p>
<ol>
<li>app-a 与 app-b　配置是否一致？——确认都是使用 S3</li>
<li>S3 是否正确配置？有没权限问题？——确认配置正确，没有权限问题</li>
<li>app-a 是否真的上传成功？——确认文件已在 S3</li>
<li>app-b 是否下载成功？——根据日志，判断下载失败，得到的信息是：文件不存在。</li>
</ol>
<p>所以问题就在于，为什么 app-b 的代码会判断文件不存在？</p>
<ul>
<li>是传过去的路径参数不对？</li>
<li>还是 app-b　的逻辑有问题？</li>
</ul>
<p>经过确认，传参是正确的，那么只有一个推论： app-b 判断文件是否存在的逻辑有问题。</p>
<p>具体是哪里有问题呢？</p>
<p>原来文件下载前，有一个判断目录是否存在的逻辑，其实现是判断 S3 的对象是否存在，示例代码如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>则该代码永远为 false。</p>
<p>如何修复呢？改为调用 <code>listObjects</code>就可以了。如下图所示（左边是修改前，右边是修改后）：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1699790489630-9003e1b5-8388-48b4-ab78-a89685efa382.png" alt=""></p>
<h2> 分析</h2>
<p>很难想像，为什么在涉及对象存储的代码中，会有判断目录是否存在的逻辑。</p>
<p>好在我是个善于为别人找理由的人。我观察了下代码库，发现接口与类结构如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/60dd2cb12a7fcb019482c4fc140f6347.svg#lake_card_v2=eyJ0eXBlIjoibWVybWFpZCIsImNvZGUiOiJjbGFzc0RpYWdyYW1cblx0ICBJU3RvcmFnZSA8fC4uIEhERlNTdG9yYWdlXG4gICAgSVN0b3JhZ2UgPHwuLiBNaW5JT1N0b3JhZ2Vcblx0XHRJU3RvcmFnZSA8fC4uIFMzU3RvcmFnZVxuXHRcdFxuICAgIGNsYXNzIElTdG9yYWdlIHtcblx0XHQgIDw8aW50ZXJmYWNlPj5cblx0XHR9XG5cdFx0XG4gICAgY2xhc3MgTWluSU9TdG9yYWdlIHtcbiAgICB9XG5cdFx0XG5cdFx0Y2xhc3MgUzNTdG9yYWdlIHsgIFxuXHRcdH1cblxuXHRcdGNsYXNzIEhERlNTdG9yYWdlIHsgIFxuXHRcdH1cblx0XHQiLCJ1cmwiOiJodHRwczovL2Nkbi5ubGFyay5jb20veXVxdWUvX19tZXJtYWlkX3YzLzYwZGQyY2IxMmE3ZmNiMDE5NDgyYzRmYzE0MGY2MzQ3LnN2ZyIsImlkIjoiZWY3N3IiLCJtYXJnaW4iOnsidG9wIjp0cnVlLCJib3R0b20iOnRydWV9LCJjYXJkIjoiZGlhZ3JhbSJ9" alt=""></p>
<p>如果定义 IStorage 与实现 S3Storage 的代码的人并不相同（我相信很可能是这样），那么我更倾向于认为责任在定义 IStorage 的人身上，因为TA并没有合理地设计接口，导致后来者被迫实现没有意义的接口，从而出错。</p>
<h2> 结论</h2>
<p>为什么开发、测试环境没问题？或者说，为什么 MinIO　没这个问题？那是因为实现 MinIOStorage 的人，没有踩这个坑。</p>
<p>所以，本此事件给我来的经验教训是什么呢？既不是 S3 如何判断目录是否存在，也不是接口定义的重要性。而是：要让测试环境与生产环境尽可能保持一致，提前暴露问题。而不是测试环境是这样的配置，生产环境又是那样的配置——这就会加大生产环境出问题的可能性！</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1699790489630-9003e1b5-8388-48b4-ab78-a89685efa382.png" type="image/png"/>
    </item>
    <item>
      <title>对Vue不得不吐槽的事</title>
      <link>https://levy.vip/daily/things-I-have-to-vent-about-vue.html</link>
      <guid>https://levy.vip/daily/things-I-have-to-vent-about-vue.html</guid>
      <source url="https://levy.vip/rss.xml">对Vue不得不吐槽的事</source>
      <description>对Vue不得不吐槽的事 为了完善博客，不得不升级至 Vuepress2。期间开发层面的各种问题我都忍了，但最终CI环节报错，我真的受不了了！ 总结一下，我对Vue生态不满的地方在于： Vue总是破坏性升级，新技术完全不管旧用户的体验 你要新技术可以，别逼我跟着用，我不想破坏 CI 的稳定性</description>
      <pubDate>Sun, 30 Jul 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 对Vue不得不吐槽的事</h1>
<p>为了完善博客，不得不升级至 Vuepress2。期间开发层面的各种问题我都忍了，但最终CI环节报错，我真的受不了了！</p>
<p>总结一下，我对Vue生态不满的地方在于：</p>
<ol>
<li>Vue总是破坏性升级，新技术完全不管旧用户的体验</li>
<li>你要新技术可以，别逼我跟着用，我不想破坏 CI 的稳定性</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>再见ChatGPT，我选择Claude2！</title>
      <link>https://levy.vip/daily/use-claude2-instead-of-chatgpt.html</link>
      <guid>https://levy.vip/daily/use-claude2-instead-of-chatgpt.html</guid>
      <source url="https://levy.vip/rss.xml">再见ChatGPT，我选择Claude2！</source>
      <description>再见ChatGPT，我选择Claude2！ 大家好，今天给大家来评测一下几个AI工具，然后做一个推荐。 首先要评测的当然是ChatGPT了，因为最早用的就是它。</description>
      <pubDate>Tue, 08 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 再见ChatGPT，我选择Claude2！</h1>
<p>大家好，今天给大家来评测一下几个AI工具，然后做一个推荐。</p>
<p>首先要评测的当然是ChatGPT了，因为最早用的就是它。</p>
<!-- more -->
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691497563137-33a17053-c9a2-47bf-bc2e-25f4d567d406.jpeg" alt="" width="200" tabindex="0"><figcaption></figcaption></figure>
]]></content:encoded>
    </item>
    <item>
      <title>Vim 作者离世</title>
      <link>https://levy.vip/daily/vim-creator-pass-away.html</link>
      <guid>https://levy.vip/daily/vim-creator-pass-away.html</guid>
      <source url="https://levy.vip/rss.xml">Vim 作者离世</source>
      <description>Vim 作者离世 R.I.P 🙏 Vim是我入行以来，一直在用的工具，可以说整个编程生涯都离不开它。 这几天听闻Vim作者离世了，便想来聊聊我与Vim的故事，以表纪念。</description>
      <pubDate>Sun, 06 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Vim 作者离世</h1>
<p>R.I.P 🙏</p>
<p>Vim是我入行以来，一直在用的工具，可以说整个编程生涯都离不开它。</p>
<p>这几天听闻Vim作者离世了，便想来聊聊我与Vim的故事，以表纪念。</p>
]]></content:encoded>
    </item>
    <item>
      <title>技术点评：别每张表都加tenant_id</title>
      <link>https://levy.vip/daily/you-dont-need-to-add-tenant_id-to-every-table.html</link>
      <guid>https://levy.vip/daily/you-dont-need-to-add-tenant_id-to-every-table.html</guid>
      <source url="https://levy.vip/rss.xml">技术点评：别每张表都加tenant_id</source>
      <description>技术点评：别每张表都加tenant_id 前言 系统满足多租户需求，是很常见的场景。本文主要聊一下在维护旧系统过程中，发现的前人多租户方案中设计、实现不合理的地方。</description>
      <pubDate>Mon, 18 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 技术点评：别每张表都加tenant_id</h1>
<h2> 前言</h2>
<p>系统满足多租户需求，是很常见的场景。本文主要聊一下在维护旧系统过程中，发现的前人多租户方案中设计、实现不合理的地方。</p>
<!-- more -->
<h2> 背景</h2>
<p>在维护旧系统时，踩了各种坑，终于忍不住在群里吐槽了下，于是有以下对话。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128515-cca8f7a3-6846-48ca-9eef-d7395c186ae2.jpeg" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128672-66e5d124-51c2-4327-b9c2-a0e72c1ba9f4.jpeg" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128413-7a5ccc98-e7ec-4fb7-94ee-d78bdc5b0bcf.jpeg" alt=""><br>
既然群友有疑问，我索性就整理了一下，把前因后果清楚。</p>
<h2> 正文</h2>
<p>1.首先，租户数据隔离级别应该如何，没有唯一标准，评价只有是否合适。因此，逻辑隔离、物理隔离，都不是吐槽点。</p>
<p>2.原来的设计是，采取逻辑隔离方案，具体做法是，给每一张表都加上了tenant_id字段。问题就在于，有必要每一张表加吗？系统的权限设计是：先有租户，再有应用，用户、角色、资源、权限设置等内容都挂在应用下，所以，应用下的内容，关联 app_id 就行了，根本不需要tenant_id。</p>
<p>3.冗余多一个字段，会出现什么问题呢？先不提查询性能、存储空间等细节，就说很实际的场景：<br>
3.1 每张表都加 tenant_id，几乎每条 sql 都要加 where tenant_id = ? ，那程序员会怎么做？首先想到的是用框架自动注入 sql<br>
3.2 在后台管理端，有一个超级管理员，能够查询出所有租户的内容，也就是说，此时查询不能带 where tenant_id = ?，也即不能让框架注入 sql</p>
<p>要同时兼容上述逻辑，程序员又会怎么做呢？于是就引入了万恶的全局变量，类似于 injectSql = true，就添加 tenant_id 作为过滤条件。但默认是不是要 injectSql = true 呢？每个项目代码又不一样，你不运行，你都不知道。</p>
<p>更恶心的是，需求变化后，是否需要带上 tenant_id 的逻辑与原来不一致时，你得在某行代码执行前，手动设置 inejctSql 的值，在该行代码之后，再手动复原——因为如果不复原，作为全局变量，会影响到后面的代码！</p>
<p>md，这时候后你才会知道，还不如老老实实地设置 tenant_id，显示地设置，好过这种隐蔽的依赖。</p>
<p>4.但这还不是最难搞的。因为上述的是代码问题，真正难搞的是数据问题。考虑一种场景：超级管理员在后台管理某租户的应用，手动为租户添加数据，请问，新增的数据 tenant_id 的值是什么，某租户的 id，还是超级管理员的id？按逻辑来说，应该是某租户的 tenant_id。但问题在于，由于理解不同，或由于疏忽让框架自动注入了 tenant_id，导致上述场景，有些数据的 tenant_id 是超级管理员的id。而又因为超级管理员进行查询时，是不带 tenant_id 作为过滤条件的，因此即使 tenant_id 的值设置错误，依然在界面上能显示，使得这个问题一直存在着，旧数据一直被保留并使用。</p>
<p>5.现在，有需求要导出某个租户下的数据，结果发现 tenant_id 乱七八糟，你难不难受？</p>
<p>6.那么，梳理完逻辑链条，我认为，虽然某些程序员的在实现上犯了低级错误，但不是主要原因，罪魁祸首应该是设计上的懒惰。设计精细一点，明确好 tenant_id 到哪张表为止，也就没有后面的 sql 注入、数据错误那么多事了。所以，我才说，给租户隔离等于给每张表加 tenant_id 的设计很傻逼！</p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695539128515-cca8f7a3-6846-48ca-9eef-d7395c186ae2.jpeg" type="image/jpeg"/>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第一册</title>
      <link>https://levy.vip/english/contemporary-college-english-1.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-1.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第一册</source>
      <description>现代大学英语精读(第2版)第一册 介绍 全书链接：https://www.ximalaya.com/album/43891910 虽然这是英语专业的大学教材，但不用担心难度——只要英语不是太差，上过大学的都能看，甚至刚参加完高考的学生就能看。 虽然名字说是精读，但自己依然可以泛读、挑选着读。因此，本文记录的是经过个人挑选认为值得一读的文章，最主要的目的是，塑造全英文阅读的习惯。除此之外，也有开阔视野，增进对西方文化、经典文学作品的了解之意。</description>
      <pubDate>Sun, 29 May 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第一册</h1>
<h2> 介绍</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/43891910" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/43891910</a></p>
<p>虽然这是英语专业的大学教材，但不用担心难度——只要英语不是太差，上过大学的都能看，甚至刚参加完高考的学生就能看。</p>
<p>虽然名字说是精读，但自己依然可以泛读、挑选着读。因此，本文记录的是经过个人挑选认为值得一读的文章，最主要的目的是，塑造全英文阅读的习惯。除此之外，也有开阔视野，增进对西方文化、经典文学作品的了解之意。</p>
<h2> 值得一读的文章</h2>
<p>本册里面大部分是记叙文，阅读趣味性比较强。</p>
<p>欧亨利不愧是大师，第一册里收录了两篇他的小说。</p>
<ul>
<li><a href="https://www.ximalaya.com/sound/356764931" target="_blank" rel="noopener noreferrer">After Twenty Years</a><br>
经典短篇小说，应该大家都在学校看过中文版。<a href="https://www.ximalaya.com/sound/356764931" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/356764931</a></li>
<li><a href="https://americanliterature.com/author/o-henry/short-story/hearts-and-hands" target="_blank" rel="noopener noreferrer">Hearts  And Hands</a><br>
我认为这篇小说是第一册中最有阅读难度的，可以作为阅读能力的检测，看能不能读懂。反正我第一次没读懂。本文使用了经典的结尾手法——最后一句话反转全文。<a href="https://americanliterature.com/author/o-henry/short-story/hearts-and-hands" target="_blank" rel="noopener noreferrer">https://americanliterature.com/author/o-henry/short-story/hearts-and-hands</a></li>
</ul>
<p>另外，戏剧 The Monsters Are Due in Maple Street 也值得一读</p>
<ul>
<li><a href="https://www.ximalaya.com/sound/357087229" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/357087229</a></li>
</ul>
<p>总结一下，本册难度较低，就是来建立信心的。如果想只阅读精华，那只看我挑选出的三篇文章就够了。适应了全英文阅读后，就可以开始看第二册的内容了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第二册</title>
      <link>https://levy.vip/english/contemporary-college-english-2.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-2.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第二册</source>
      <description>现代大学英语精读(第2版)第二册 介绍 全书链接：https://www.ximalaya.com/album/44290107 本册多了一些议论文，难度有所增加。同时记叙文也更加有内涵，立意高了不少。 另外就是，语言难度有所上升——比如一些骂人的话，单词能认识，变成句子就不懂了😂。 值得一读的文章 say-yes-by-tobias-wolff 第一次没读懂，看到最后，我还以为家里进小偷了 😂</description>
      <pubDate>Sat, 04 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第二册</h1>
<h2> 介绍</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/44290107" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/44290107</a></p>
<p>本册多了一些议论文，难度有所增加。同时记叙文也更加有内涵，立意高了不少。 另外就是，语言难度有所上升——比如一些骂人的话，单词能认识，变成句子就不懂了😂。</p>
<h2> 值得一读的文章</h2>
<p>say-yes-by-tobias-wolff 第一次没读懂，看到最后，我还以为家里进小偷了 😂</p>
<ul>
<li></li>
</ul>
<p>原文：<a href="https://www.missmccalister.com/uploads/3/0/9/3/30937509/lesson-7a-say-yes-by-tobias-wolff.pdf" target="_blank" rel="noopener noreferrer">https://www.missmccalister.com/uploads/3/0/9/3/30937509/lesson-7a-say-yes-by-tobias-wolff.pdf</a></p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Say_Yes_(short_story)" target="_blank" rel="noopener noreferrer">wiki</a><br>
说明：<a href="https://en.wikipedia.org/wiki/Say_Yes_(short_story)" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Say_Yes_(short_story)</a></li>
</ul>
<p>The Dog of Pompeii 讲述了一个盲人男孩与一只小狗的故事，令人泪目。结尾比较含蓄，思考片刻明白后，就破防了～</p>
<ul>
<li></li>
</ul>
<p>原文：<a href="https://www.acaedu.net/cms/lib3/TX01001550/Centricity/Domain/562/Week%206%20-%20The%20Dog%20of%20Pompeii.pdf" target="_blank" rel="noopener noreferrer">https://www.acaedu.net/cms/lib3/TX01001550/Centricity/Domain/562/Week%206%20-%20The%20Dog%20of%20Pompeii.pdf</a></p>
<p>button button 人心拷问：如果按下按钮，你能得到一笔钱，但世上会有一人因此而死去，你会按下按钮吗？</p>
<ul>
<li></li>
</ul>
<p>原文：<a href="https://christian_fuller.myteachersite.org/teacher/files/documents/button%20button.pdf" target="_blank" rel="noopener noreferrer">https://christian_fuller.myteachersite.org/teacher/files/documents/button%20button.pdf</a></p>
<p>A Doctor's Dilemma 医生的困境</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201706/508290.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201706/508290.shtml</a></li>
<li>拓展阅读，<a href="https://www.gutenberg.org/files/5070/5070-h/5070-h.htm" target="_blank" rel="noopener noreferrer">肖伯纳的 THE DOCTOR’S DILEMMA</a><br>
：<a href="https://www.gutenberg.org/files/5070/5070-h/5070-h.htm" target="_blank" rel="noopener noreferrer">https://www.gutenberg.org/files/5070/5070-h/5070-h.htm</a></li>
</ul>
<p>the-oyster-and-the-pearl</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201708/521462.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201708/521462.shtml</a></li>
<li></li>
</ul>
<p>解析：<a href="https://schoolworkhelper.net/william-saroyans-the-oyster-and-the-pearl-summary-analysis/" target="_blank" rel="noopener noreferrer">https://schoolworkhelper.net/william-saroyans-the-oyster-and-the-pearl-summary-analysis/</a></p>
<p>文册收录了两篇奥巴马的演讲，还是挺有意思的，能打动人心的部分当然有，同时也可以看出一些政治家演讲的特色：</p>
<ol>
<li>赞美对方。糖衣炮弹过去，对方就伸手不打笑脸人</li>
<li>用好词，美化自己，把功劳往自己身上揽</li>
<li>强调双方关系： 我们是很友好的哦、我们是合作伙伴哦</li>
<li>回顾历史，展望未来：我们已经走了很长的路，做出了很多的改变，取得了很大的进步，为了下一代、为了未来，我们要加油鸭！</li>
</ol>
<p>相关链接：</p>
<ul>
<li>竞选胜利演讲：<a href="http://www.kekenet.com/daxue/201707/518237.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201707/518237.shtml</a></li>
<li>在上海的演讲：<a href="http://www.kekenet.com/daxue/201708/518540.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201708/518540.shtml</a></li>
</ul>
<p>另外，我摘录了一些觉得不错的句子：</p>
<ul>
<li>When dealing with people, let us remember we are not dealing with creatures of logic.</li>
<li>We are dealing with creatures of emotion, creatures bristling with prejudices, and motivated by pride and vanity.</li>
<li>Any fool can criticize, condemn and complain—and most fools do. But it takes character and self-control to be<br>
understanding and forgiving.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第三册</title>
      <link>https://levy.vip/english/contemporary-college-english-3.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-3.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第三册</source>
      <description>现代大学英语精读(第2版)第三册 介绍 全书链接：https://www.ximalaya.com/album/44439108 阅读本册，我在阅读上开始有了一定的厌倦感。这种厌倦感来自于，文章看得多了，不再首先关注是否完整读懂了文章，而是更关心文章是否有趣、是否吸引人——如果不能吸引我，我都不想去关心你在说什么。所以，有些说明议论文，我是跳着看的，因为某些段落让我觉得很无聊，内容不吸引我，行文呆板僵硬，充满了说教的味道。</description>
      <pubDate>Sat, 25 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第三册</h1>
<h2> 介绍</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/44439108" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/44439108</a></p>
<p>阅读本册，我在阅读上开始有了一定的厌倦感。这种厌倦感来自于，文章看得多了，不再首先关注是否完整读懂了文章，而是更关心文章是否有趣、是否吸引人——如果不能吸引我，我都不想去关心你在说什么。所以，有些说明议论文，我是跳着看的，因为某些段落让我觉得很无聊，内容不吸引我，行文呆板僵硬，充满了说教的味道。</p>
<p>这种转变，说明了两点：</p>
<ol>
<li>我开始应用我在中文阅读中培养出来的阅读喜爱，去评判英文文章了</li>
<li>我开始有意识地运用阅读技巧，恰当地泛读，不再是拿到一篇文章就逐字逐句地读</li>
</ol>
<h2> 值得一读的文章</h2>
<p>A-DILL-PICKLE 讲述的是曾经的恋人偶遇，欧亨利式结尾：</p>
<ul>
<li></li>
</ul>
<p>原文：<a href="https://www.katherinemansfieldsociety.org/archive/www.katherinemansfieldsociety.org/assets/KM-Stories/A-DILL-PICKLE1917.pdf" target="_blank" rel="noopener noreferrer">https://www.katherinemansfieldsociety.org/archive/www.katherinemansfieldsociety.org/assets/KM-Stories/A-DILL-PICKLE1917.pdf</a></p>
<ul>
<li>wiki<br>
解析：<a href="https://en.wikipedia.org/wiki/A_Dill_Pickle#Plot_summary" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/A_Dill_Pickle#Plot_summary</a></li>
</ul>
<p>The Invisible Japanese Gentlemen</p>
<ul>
<li></li>
</ul>
<p>原文：<a href="https://www.ff.umb.sk/app/cmsFile.php?disposition=a&amp;ID=4292" target="_blank" rel="noopener noreferrer">https://www.ff.umb.sk/app/cmsFile.php?disposition=a&amp;ID=4292</a></p>
<ul>
<li><a href="http://sittingbee.com/the-invisible-japanese-gentlemen-graham-greene/" target="_blank" rel="noopener noreferrer">分析：http://sittingbee.com/the-invisible-japanese-gentlemen-graham-greene/</a></li>
</ul>
<p>My Grandmother, the Bag Lady 被这篇文章打动，泪目了</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201704/504622.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201704/504622.shtml</a></li>
</ul>
<p>The End of the Civil War 本册收录了挺多美国南北战争相关的内容，这是其中角度新颖的一篇</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201705/509083.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201705/509083.shtml</a></li>
</ul>
<p>Why Historians Disagree 科普了一下历史学是如何看待历史，有助于我们更理性地学习历史</p>
<ul>
<li>原文：<a href="https://www.ximalaya.com/sound/361971944" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/361971944</a></li>
</ul>
<p>the-most-dangerous-game 可以说是三册中最精彩的一篇小说之一，不容错过</p>
<ul>
<li></li>
</ul>
<p>原文+分析（左边是内容，右边是分析）：<a href="https://www.litcharts.com/lit/the-most-dangerous-game/summary-and-analysis" target="_blank" rel="noopener noreferrer">https://www.litcharts.com/lit/the-most-dangerous-game/summary-and-analysis</a></p>
<p>The Bench 用一个黑人小哥故意坐在标明只能由欧洲人坐的板凳上的故事，以小见大，描绘了 Civil War 之后美国种族歧视现状</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201708/521739.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201708/521739.shtml</a></li>
</ul>
<p>Twelve Angry Men 十二怒汉，是本册的精彩内容之一。激发了对法治话题的兴趣，看得我直接睡不着，接着去B站看电影版。</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201708/522771.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201708/522771.shtml</a></li>
<li></li>
</ul>
<p>电影：<a href="https://www.bilibili.com/bangumi/play/ep332629?theme=movie&amp;spm_id_from=333.337.0.0" target="_blank" rel="noopener noreferrer">https://www.bilibili.com/bangumi/play/ep332629?theme=movie</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第四册</title>
      <link>https://levy.vip/english/contemporary-college-english-4.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-4.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第四册</source>
      <description>现代大学英语精读(第2版)第四册 介绍 全书链接：https://www.ximalaya.com/album/44641280 本册立意又高了一个层次，开始讨论哲学、政治等上层建筑，并提供了新颖的视角。如果说，前几册是开阔视野，本册开始拓展思维了。 值得一读的文章 Groundless Beliefs 提醒我们，自认为正确的东西，很可能只是毫无根据的盲从</description>
      <pubDate>Mon, 11 Jul 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第四册</h1>
<h2> 介绍</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/44641280" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/44641280</a></p>
<p>本册立意又高了一个层次，开始讨论哲学、政治等上层建筑，并提供了新颖的视角。如果说，前几册是开阔视野，本册开始拓展思维了。</p>
<h2> 值得一读的文章</h2>
<p>Groundless Beliefs 提醒我们，自认为正确的东西，很可能只是毫无根据的盲从</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201808/561902.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201808/561902.shtml</a></li>
</ul>
<p>Economic Growth Is a Path to Perdition, Not Prosperity 提醒我们更加理性地看待“增长”</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201811/571277.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201811/571277.shtml</a></li>
</ul>
<p>The Damned Human Race 其实我觉得本文并非“值得一读”，但还列举出来原因有二：一是毕竟是马克·吐温之作，不可怠慢；二是我认为其论点新颖，不愧为讽刺大师，但论据越到后面越无说服力，可列为“反面教材”</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201812/573461.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201812/573461.shtml</a></li>
</ul>
<p>A String of Beads 挺有趣的文章，揭示了人们普遍的一种心理：事情的真相相比，人们更在意的是故事的戏剧性，或是否符合自己心中的设定</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201812/574998.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201812/574998.shtml</a></li>
</ul>
<p>The World House 马丁·路德·金 的文章</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201901/576160.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201901/576160.shtml</a></li>
</ul>
<p>Soldier's Heart 也叫PTSD，由战后士兵自述</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201902/578424.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201902/578424.shtml</a></li>
</ul>
<p>Secrets 这是一篇可以让人由好奇变为感动的文章。我原以为讲的是主人公的 “aunt” 与其旧情人的故事，直到最后，“forgive”一词的出现，令我恍然大悟，瞬间泪目。为避免剧透，我不细说了，强烈推荐。</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201903/580020.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201903/580020.shtml</a></li>
</ul>
<p>The Rivals<br>
两个男的在言语上较劲，只觉得好笑，可看到后面，我心情如此图<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1688258250066.png" alt="1657000860176_9DFDD7F8-C809-4aa4-A996-05F4C984C76A.png"><br>
强烈推荐，你一定也会“surprised”!</p>
<ul>
<li>原文：<a href="http://www.kekenet.com/daxue/201904/584021.shtml" target="_blank" rel="noopener noreferrer">http://www.kekenet.com/daxue/201904/584021.shtml</a></li>
</ul>
<p>Cord：一对母女的故事</p>
<ul>
<li>原文：<a href="https://www.ximalaya.com/sound/364021411" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/364021411</a></li>
<li></li>
</ul>
<p>论文分析。有一定难度，因为它是综合了同一个作者的三篇文章进行分析，需要挑选着读：<a href="https://www.yuque.com/office/yuque/0/2022/pdf/160590/1657283857640-11aa59bb-edc1-475f-a97f-37124a27afd9.pdf?from=https%3A%2F%2Fwww.yuque.com%2Flevy%2Fblog%2Fxweufx%2Fedit" target="_blank" rel="noopener noreferrer">https://www.yuque.com/office/yuque/0/2022/pdf/160590/1657283857640-11aa59bb-edc1-475f-a97f-37124a27afd9.pdf</a></p>
<p>The Never-Ending Fight “机器人学之父”阿西莫夫的反迷信文章</p>
<ul>
<li>原文：<a href="https://www.ximalaya.com/sound/364032034" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/364032034</a></li>
</ul>
<p>Man of the Moment 又到了令人享受的戏剧欣赏时间</p>
<ul>
<li>原文：<a href="https://www.ximalaya.com/sound/364032690" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/364032690</a></li>
</ul>
<p>Is Everybody Happy 讨论了幸福的定义</p>
<ul>
<li>
<p>原文：<a href="https://www.ximalaya.com/sound/364033142" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/364033142</a></p>
<p><a href="https://americanliterature.com/author/o-henry/short-story/hearts-and-hands" target="_blank" rel="noopener noreferrer"><br>
</a></p>
</li>
</ul>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1688258250066.png" type="image/png"/>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第五册</title>
      <link>https://levy.vip/english/contemporary-college-english-5.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-5.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第五册</source>
      <description>现代大学英语精读(第2版)第五册 介绍 全书链接：https://www.ximalaya.com/album/49466046 本册开始，阅读难度再次上升，同时也更有思考的乐趣。另外，从本册开始，会在后面推荐一些著名的英文演讲，其中就有大家熟悉的乔布斯，激发读者的热情。 Who Are You and What Are You Doing Here</description>
      <pubDate>Sun, 28 Aug 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第五册</h1>
<h2> 介绍</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/49466046" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/49466046</a></p>
<p>本册开始，阅读难度再次上升，同时也更有思考的乐趣。另外，从本册开始，会在后面推荐一些著名的英文演讲，其中就有大家熟悉的乔布斯，激发读者的热情。</p>
<h2> Who Are You and What Are You Doing Here</h2>
<p>原文链接：<a href="https://m.kekenet.com/daxue/201909/593904.shtml" target="_blank" rel="noopener noreferrer">https://m.kekenet.com/daxue/201909/593904.shtml</a></p>
<p>本文分享了作者教育观，并阐述了上大学的意义，否定了上学是为了就业的观点。</p>
<p>摘录如下： Education has one salient enemy in present-day America, and that enemy is education——university education in<br>
particular. To almost every university education is a means to an end. For students, that end is a good job.</p>
<p>The best reason to read great writers is to see if they may know you better than you know yourself. You may find your<br>
own suppressed and rejected thoughts flowing back to you with an "alienated majesty." Reading the great writers, you may<br>
have the experience that Longinus associated with the sublime: You feel that you have actually created the text<br>
yourself. For somehow your predecessors are more yourself than you are.</p>
<p>Trying to figure out whether the stuff you're reading is true or false and being open to having your life changed is a<br>
fraught, controversial activity. Doing so requires energy from the professors. This kind of perspective-altering<br>
teaching and learning can cause the things which administrators fear above all else: trouble, arguments, bad press, etc.</p>
<h2> <strong>Two Kinds</strong></h2>
<p>原文链接：<a href="https://m.kekenet.com/daxue/201909/595380.shtml" target="_blank" rel="noopener noreferrer">https://m.kekenet.com/daxue/201909/595380.shtml</a></p>
<p>本文以女儿的视角讲述了母亲望女成凤而女儿叛逆的故事，文中母亲代表的中国式家庭教育观，我想不少人会感同身受。用现在的眼光看来，这只不过是一段充满了失败的沟通、无法双赢的家庭教育经历，借助非暴力沟通或双赢思维是能够避免的。</p>
<p>摘录如下： I didn't budge. And then I decided. I didn't have to do what my mother said anymore. I wasn't her slave. This<br>
wasn't China. I had listened to her before and look what happened. She was the stupid one.</p>
<p>You want me to be someone that I'm not!" I sobbed. "I will never be the kind of daughter you want me to be!"</p>
<p>"Only two kinds of daughters," she shouted in Chinese. "Those who are obedient and those who follow their own mind! Only<br>
one kind of daughter can live in this house. Obedient daughter!"</p>
<p>"Then I wish I wasn't your daughter, I wish you weren't my mother," I shouted.</p>
<h2> Love is a Fallacy</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414753193" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414753193</a></p>
<p>这多篇看下来，这篇是目前最有意思、最有趣、最令人忍俊不禁的。为避免剧透，我就少说一点。本文首尾响应，完美闭环，看完后我直呼我直呼，哈哈哈哈。就说到这了，总之墙裂推荐。</p>
<h2> Rewriting American History</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414754775" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414754775</a></p>
<p>本文讲述的是美国历史教材的一些变化及背后的思考。我认可其中的一个做法：不是对历史给一个所谓正统的结论，而是充分准备材料，从不同的角度陈述观点，引导人思考。</p>
<h2> Nobel Peace Price About Global Warming</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414756339" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414756339</a></p>
<h2> The Bluest Eyes</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414757228" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414757228</a></p>
<p>这是个记叙文，然而，我认为确是这么多篇文章中，最难懂的。</p>
<h2> How News Becomes Options and Opinions Off-Limits</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414759448" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414759448</a></p>
<p>本文说明了当代一种社会现代：人们在看新闻，不是在看“事实”，而是在看“观点”，作者认为，现在的新闻跟小说并非没有相似之处了。然而，作者对这种现象并非完全持否定态度，他认为这是言论自由的一种方式。最后，作者还表达了在自由社会对“尊重”一词的看法。</p>
<p>摘录如下： In any version of a free society, the value of free speech must rank the highest, for that is the freedom without<br>
which all other freedoms would fall.</p>
<p>In free societies, you must have the free play of ideas. There must be arguments, and it must be impassioned and<br>
untrammeled.</p>
<p>A free society is not a calm and eventless place——that is the kind of static, dead society dictators try to create. Free<br>
societies are dynamic, noisy, turbulent and full of radical disagreements.</p>
<h2> The Indispensable Opposition</h2>
<p>原文链接：<a href="https://m.kekenet.com/daxue/202001/603745.shtml" target="_blank" rel="noopener noreferrer">https://m.kekenet.com/daxue/202001/603745.shtml</a></p>
<p>本文所表达的观点，属于民主与自由思想的经典内容：不可缺少的反对派（或忠诚的反对派）。</p>
<p>摘录如下： I wholly disapprove of what you say, but will defend to the death your right to say it.</p>
<p>If we truly wish to understand why freedom is necessary in a civilized society, we must begin by realizing that, because<br>
freedom of discussion improves our own opinions, the liberties of other men are our own vital necessity.</p>
<p>We must insist that free oratory is only the beginning of free speech; it is not the end, but a means to an end. The end<br>
is to find the truth.</p>
<p>The only reason for dwelling on all this is that if we are to preserve democracy we must understand its principles. And<br>
the principle which distinguishes it from all other forms of government is that in a democracy the opposition not only<br>
is tolerated as constitutional but must be maintained because it is in fact indispensable.</p>
<p>The democratic system cannot be operated without effective opposition. For, in making the great experiment of governing<br>
people by consent rather than by coercion, it is not sufficient that the party in power should have a majority. It is<br>
just as necessary that the party in power should never outrage the minority. That means that it must listen to the<br>
minority and be moved by the criticisms of the minority. That means that its measures must take account of the<br>
minority's objections, and that in administering measures it must remember that the minority may become the majority.</p>
<h2> The Danger of a Single Story</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414765357" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414765357</a></p>
<p>认识到硬币有正反面，并坚持凡事至少从两个角度去看待与分析，那么本篇文章就没白读。</p>
<p>摘录如下： It is impossible to talk about the single story without talking about power. Power is the ability not just to tell<br>
the story of another person, but to make it the definitive story of that person.</p>
<p>All of these stories make me who l am. But to insist on only these negative stories is to flatten my experience and to<br>
overlook the many other stories that formed me.</p>
<p>The single story creates stereotypes,and the problem with stereotypes is not that theyare untrue, but that they are<br>
incomplete. They make one story become the only story.</p>
<p>The consequence of the single story is this:  It robs people of dignity. It makes our recognition of our equal humanity<br>
difficult. It emphasizes how we are different rather than how we are similar.</p>
<h2> Come Rain or Come Shine</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414767340" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414767340</a></p>
<p>看得不太懂，但故事倒挺有意思的。</p>
<p>解析：<a href="https://medium.com/the-afterglow/come-rain-or-come-shine-by-kazuo-ishiguro-finding-the-moment-b09e418652db" target="_blank" rel="noopener noreferrer">https://medium.com/the-afterglow/come-rain-or-come-shine-by-kazuo-ishiguro-finding-the-moment-b09e418652db</a></p>
<h2> Invisible Man</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414776047" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414776047</a></p>
<p>节选自同名小说（有改编），telling a story about a black teenager present his speech before white people.</p>
<h2> You've Got to Find What You Love</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414769322" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414769322</a></p>
<p>Steve Jobs 在 Standford 的演讲，没想到 Jobs 声音这么年轻！演讲分享三个故事，表达主旨“follow your heart”。</p>
<p>精彩摘录如下： You can't connect the dots looking forward: You can only connect them looking backwards. So you have to trust<br>
that the dots will somehow connect in your future. You have to trust in something-your destiny,life,whatever. Because<br>
believing that the dots will connect down the road will give you the confidence to follow your heart, even when it leads<br>
you off the well-worn path. And that would make all the difference.</p>
<p>Sometimes life is gonna hit you in the head with a brick. Don't lose faith. I'm convinced that the only thing that kept<br>
me going was that I loved what I did. You've got to find what you love. And that is as true for your work as it is for<br>
your lovers. Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what<br>
you believe is great work. And the only way to do great work is to love what you do. If you haven't found it yet, keep<br>
looking, and don't settle. As with all matters of the heart, you'll know when you find it. And like any great<br>
relationship, it just gets better and better as the years roll on. So keep looking. Don't settle.</p>
<p>I have looked in the mirror every morning and asked myself: "If today were the last day of my life,would I want to do<br>
what l am about to do today?" And whenever the answer has been "No" for too many days in a row, l know I need to change<br>
something. Remembering that I'll be dead soon is the most important tool I've ever encountered to help me make the big<br>
choices in life. Because almost everything-all external expectations, all pride, all fear of embarrassment or<br>
failure-these things just fall away in the face of death,leaving only what is truly important. Remembering that you are<br>
going to die is the best way I know to avoid the trap of thinking you have something to lose. You are already naked.<br>
There is no reason not to follow your heart.</p>
<h2> Where Do We Go from Here</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414771197" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414771197</a></p>
<p>本文是马丁·路德·金的另一篇呼吁黑人平等的演讲，从中可以看到与《I Have a Dream》的影子。</p>
<p>摘录如下： As long as the mind is enslaved, the body can never be free. Psychological freedom,a firm sense of self-esteem, is<br>
the most powerful weapon against the long night of physical slavery. No Lincolnian emancipation proclamation or<br>
Johnsonian civil rights bill can totally bring this kind of freedom. The Negro will only be free when he reaches down to<br>
the inner depths of his own being and signs with the pen and ink of assertive manhood his own emancipation proclamation.</p>
<p>And so I say to you today that I still stand by nonviolence. For through violence you may murder a murderer but you<br>
can't murder murder. Through violence you may murder a liar but you can't establish truth. Through violence you may<br>
murder a hater,but you can't murder hate. Darkness cannot put out darkness. Only light can do that.</p>
]]></content:encoded>
    </item>
    <item>
      <title>现代大学英语精读(第2版)第六册</title>
      <link>https://levy.vip/english/contemporary-college-english-6.html</link>
      <guid>https://levy.vip/english/contemporary-college-english-6.html</guid>
      <source url="https://levy.vip/rss.xml">现代大学英语精读(第2版)第六册</source>
      <description>现代大学英语精读(第2版)第六册 前言 全书链接：https://www.ximalaya.com/album/49468954 本册是整个系列的最后一册了，完结撒花🎉 Paper Tigers 原文链接：https://www.ximalaya.com/sound/414998175</description>
      <pubDate>Sun, 04 Sep 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 现代大学英语精读(第2版)第六册</h1>
<h2> 前言</h2>
<p>全书链接：<a href="https://www.ximalaya.com/album/49468954" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/album/49468954</a></p>
<p>本册是整个系列的最后一册了，完结撒花🎉</p>
<h2> Paper Tigers</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414998175" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414998175</a></p>
<p>探讨了 Asian American 的教育经历与社会成就不符的现象。</p>
<p>摘录如下： Let me summarize my feelings toward Asian values: Damn filial piety. Damn grade grubbing. Damn Ivy League mania.<br>
Damn deference to authority. Damn humility and hard work. Damn harmonious relations. Damn sacrificing for the future.<br>
Damn earnest, striving middle-class servility.</p>
<p>Maybe a traditionally Asian upbringing is the problem. In order to be a leader, you must have followers. Associates are<br>
initially judged on how well they do the work they are assigned. But being a leader requires different skill sets. “The<br>
traits that got you to where you are won't necessarily take you to the next level," says the diversity consultant Jane<br>
Hyun, who wrote a book called Breaking the Bamboo Ceiling.</p>
<p>At Yale, Chua made the connection between her upbringing and her adult dissatisfaction. “My parents didn't sit around<br>
talking about politics and philosophy at the dinner table," she told the students. Even after she had escaped from<br>
corporate law and made it onto a law faculty,“I was kind of lost. I just didn't feel the passion."</p>
<p>Chua's Chinese education had gotten her through an elite schooling, but it left her unprepared for the real world.</p>
<p>注：Chua<br>
也就是《虎妈战歌》的作者：<a href="https://baike.baidu.com/item/%E8%94%A1%E7%BE%8E%E5%84%BF/3424632#7" target="_blank" rel="noopener noreferrer">https://baike.baidu.com/item/%E8%94%A1%E7%BE%8E%E5%84%BF/3424632#7</a></p>
<h2> What Is News</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/414999846" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/414999846</a></p>
<p>提示我们如何看待新闻，与前面的如何看待历史可谓是姐妹篇。</p>
<p>摘录如下： The news is made rather than gather.</p>
<p>We see what we expect to see. We focus on what we are paid to see. And those who pay us to see usually expect us to<br>
accept their notions.</p>
<p>“What is news?" News,we might say, may be history in its first and best form, or the stuff of literature, or a record of<br>
the condition of a society, or the expression of things, but in its worst form it can also be mainly a “filler,” a<br>
“come-on" to keep the viewer's attention until the commercials come.</p>
<p>All of which leads us to reiterate, first, that there are no simple answers to the question “What is news?"<br>
And, second, that it is not our purpose to tell you what you ought to believe about the question. The purpose of this<br>
chapter is to arouse your interest in thinking about the question. Your answers are to be found by knowing what you feel<br>
is significant and how your sense of the significant conforms with or departs from that of others. Answers are to be<br>
found in your ideas about the purpose of public communication, and in your judgment of the kind of society you live in<br>
and wish to live in. We cannot provide answers to these questions. But you also need to know something about the<br>
problems, limitations, traditions, motivations, and, yes, even the delusions of the television news industry.</p>
<h2> At War with the Planet</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415001340" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415001340</a></p>
<p>本文为了我们科普了人类社会与自然环境的特征，提供了一个新颖而深刻的视角来看待人与自然的关系，启发我们在采取人与自然和谐相处的措施时，不要走极端。</p>
<h2> How to Get the Poor off Our Conscience</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415004615" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415004615</a></p>
<p>本文讨论了面对一直存在的贫富差距现象，社会思想、政府措施在历史上经历了怎样的变化。</p>
<h2> Housewifely Arts</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415006607" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415006607</a></p>
<p>本文主要表达的是对母亲的回忆，其实属于可看可不看类型，之所以还放上来，原因有二：</p>
<ol>
<li>冷幽默，毕竟有几处地方让我笑了</li>
<li>引以为戒，千万别学女主</li>
</ol>
<h2> The One Against The Many</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415007539" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415007539</a></p>
<p>本文主要是讲美国文明的，没有一个地方出现China，但几乎处处都能看到China的影子。绝对不容错过的好文，可以说是反洗脑的基础。</p>
<p>精彩摘录： It is important here to insist on the distinction between ideals and ideology. Ideals refer to the long-run goals<br>
of a nation and the spirit in which these goals are pursued. Ideology is something different, more systematic, more<br>
detailed, more comprehensive, more dogmatic.</p>
<p>An ideology, in other words, is an abstraction from reality. There is nothing wrong with abstractions or models. In<br>
fact, we could not conduct discourse without them. The ideological fallacy is to forget that ideology is an abstraction<br>
from reality and to regard it as reality itself. The besetting sin of the ideologist, in short, is to confuse his own<br>
tidy models with the vast, turbulent, unpredictable, and untidy reality which is the stuff of human experience.</p>
<p>Consider for a moment the ideologist's view of history. The ideologist contends that the mysteries of history can be<br>
understood in terms of a clear-cut, absolute, social creed which explains the past and forecasts the future. Ideology<br>
thus presupposes a closed universe whose history is determined, whose principles are fixed, whose values and objectives<br>
are deducible from a central body of social dogma and often whose central dogma is confided to the custody of an<br>
infallible priesthood.</p>
<p>The American tradition has found this view of human history repugnant and false, against the belief in the<br>
all-encompassing power of a single explanation, against the commitment to the absolutism of ideology, against the notion<br>
that all answers to political and social problems can be found in the back of some sacred book, against the<br>
deterministic interpretation of history.</p>
<p>Ideologists are afraid of the free flow of ideas, even of deviant ideas within their own ideology. They are convinced<br>
they have a monopoly on the Truth. Therefore they always feel that they are only saving the world when they slaughter<br>
the heretics. Their objective remains that of making the world over in the image of their dogmatic ideology. The goal is<br>
a monolithic world, organized on the principle of infallibility-but the only certainty in an absolute system is the<br>
certainty of absolute abuse.</p>
<p>The goal of free men is quite different. Free men know many truths, but they doubt whether any mortal man knows the<br>
Truth. Their religious and their intellectual heritage join in leading them to suspect fellow men who lay claim to<br>
infallibility. They believe that there is no greater delusion than for man to mistake himself for God. They accept the<br>
limitations of the human intellect and the infirmity of the human spirit.</p>
<h2> Notes on the English Character</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415017440" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415017440</a></p>
<p>本文对英格兰人的性格特征进行了简单的探讨。</p>
<h2> The Death of a Pig</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415019607" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415019607</a></p>
<p>本文主人公本计划养一只猪来吃，却养死了。为何把这篇文章收录进来呢，可能是因为里面细节写得好吧，把主人公的病急乱投医的场景写得较为生动。</p>
<h2> Don't Eat Fortune's Cookie</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415020743" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415020743</a></p>
<p>Princeton学长回校演讲，核心观点是：你以为你的成功全靠实力，然而其中隐含着运气。</p>
<h2> The Accidental Universe</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415022329" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415022329</a></p>
<p>本文是物理相关的说明文，讲了多重宇宙假设，提出一种观点：我们所在的宇宙是出于偶然。</p>
<h2> Rowling's Speech at Harvard</h2>
<p>原文链接：<a href="https://www.ximalaya.com/sound/415018537" target="_blank" rel="noopener noreferrer">https://www.ximalaya.com/sound/415018537</a></p>
<p>罗琳的英音很好听，而且本次演讲也很幽默，建议边看边听。</p>
]]></content:encoded>
    </item>
    <item>
      <title>人人都能学会的英语1：开篇</title>
      <link>https://levy.vip/english/everyone-can-learn-english-1-overview.html</link>
      <guid>https://levy.vip/english/everyone-can-learn-english-1-overview.html</guid>
      <source url="https://levy.vip/rss.xml">人人都能学会的英语1：开篇</source>
      <description>人人都能学会的英语1：开篇 为什么学 不可否认，英语始终在工作、生活中扮演着不可或缺的角色。可能有人并未意识到，我们的科学技术、政治经济、文化娱乐等领域都深受着英语世界的影响。掌握英语，运用英语，始终是非常必要的。 也许有人会反驳：我又不出国，我又不去外企上班，学英语有啥用？嗯，说得有道理，这属于实用主义。那么按此逻辑，但凡有以下需求，就不得不承认要学英语： 出国 去外企 有国际化需求 学习专业领域的前沿知识 我们尤其关注最后一点，它是与本职业相关的。以程序员为例，很多新的技术都起源于西方世界，往往文档资料都是先有英文版的，想要中文版至少要半年甚至更久，要想技术快人一步，职业发展更上一层楼，英语能力必不可少。</description>
      <pubDate>Thu, 23 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 人人都能学会的英语1：开篇</h1>
<h2> 为什么学</h2>
<p>不可否认，英语始终在工作、生活中扮演着不可或缺的角色。可能有人并未意识到，我们的科学技术、政治经济、文化娱乐等领域都深受着英语世界的影响。掌握英语，运用英语，始终是非常必要的。</p>
<p>也许有人会反驳：我又不出国，我又不去外企上班，学英语有啥用？嗯，说得有道理，这属于实用主义。那么按此逻辑，但凡有以下需求，就不得不承认要学英语：</p>
<ul>
<li>出国</li>
<li>去外企</li>
<li>有国际化需求</li>
<li>学习专业领域的前沿知识</li>
</ul>
<p>我们尤其关注最后一点，它是与本职业相关的。以程序员为例，很多新的技术都起源于西方世界，往往文档资料都是先有英文版的，想要中文版至少要半年甚至更久，要想技术快人一步，职业发展更上一层楼，英语能力必不可少。</p>
<p>此时也许有人会担心，自己已错过了最佳的学习时机，现在太迟了吧？非也。种一棵树最好的时机是10年前，其次是现在。如果把人生当成马拉松，那么现在开始，一定来得及，绝不会晚。记住，Better late than never!</p>
<h2> 怎么学</h2>
<p>关于怎么学的问题，我认为需要内外兼修。</p>
<h3> 建设心态</h3>
<p>在意识到、并承认掌握英语的必要性后，我们需要真正地相信：现在行动并不晚。</p>
<p>如果这点不能相信，那说什么都没用，因为人会找各种借口让自己放弃，如“现在没时间，还有更重要的事”、“性价比太低了”、“以后再说吧”、“用中文也行”。如果是这样，那事情注定是要失败的。</p>
<p>其次，要学会耐心。人们常常高估一个月所能做的事，却低估一年所能做的事。不要浮躁，学会接受每天进步一点点：</p>
<p>有了耐心，才能在整个过程中，抵制住营销号“神奇方法”的诱惑。记住，耐心是美德，这在别的领域也适用。</p>
<p>与此同时，在学习过程中，注意建立自己的信心。从怀疑自己是否能，到相信自己能，最后内化成不再需要强调这件事。</p>
<h3> 提升认知</h3>
<p>一定要搞明白：英语是技能，不只是知识。技能意味着，不仅要输入，还要输出，且需要反复地练习。</p>
<p>所以，在学习过程中，一定要让输入与输出闭环：如果想提高听力，就要辅以口语练习；如果想提高阅读能力，就要做相应的写作练习。</p>
<p>整个过程中，强调的是实践，是行动力，而不是记忆力。不要妄想把大量的知识装入脑子里——那是没用的，因为它不属于你。只有经过练习、能说出来或写出来的东西，才是你的，才会内化成自己的能力。</p>
<p>以上认知很重要，如果不能达成共识，那后面的方法对你是无效的。</p>
<h3> 明确意义</h3>
<p>需要问一下自己，英语对自己而言到底是什么？</p>
<p>是考试、考证上分的手段，还是升职加薪的工具，抑或是丰富自己、了解另一种文化、开阔视野的方式？</p>
<p>上面只是举例，并且例子之间并不冲突，是可以兼容的。</p>
<p>总之，对这个问题的回答，反映了个人动机，对行动力有很大的影响。</p>
<h3> 制定计划</h3>
<p>整个学习过程是超过一个月的，对于这种时长的事件，是需要制定计划的。</p>
<p>说直白了，就是要拆分目标，评估工作量，然后按计划执行，并能在实践中结合自身情况动态调整。这种能力，不仅是学英语，学任何东西都是适用的。</p>
<h3> 培养习惯</h3>
<p>注意培养自己的习惯，利用惯性帮助自己坚持，减少半途而废的可能。</p>
<p>建议固定时间、固定地点来学习，可以结合自己的日常作息规律，养成一个新的习惯。如每天早上起来，或晚上洗完澡后，固定抽时间来学习。</p>
<h3> 注重方法</h3>
<p>前面说了那么多，都是内功，放在前面，是因为我认为心态建设、认知及习惯比方法更重要。</p>
<p>至于学习方法，因学习内容而异。按照知识体系划可大致分为：</p>
<ul>
<li>基础知识：
<ul>
<li>音标</li>
<li>单词</li>
<li>语法</li>
</ul>
</li>
<li>综合能力
<ul>
<li>听说</li>
<li>读写</li>
</ul>
</li>
</ul>
<p>基础知识是一定要掌握的，而对于综合能力，基于实用主义，不同目的可有不同的侧重。简单来说就是：</p>
<ul>
<li>如果需要利用英文资料学习、工作，或想阅读英文原著，或只需与人进行文字交流，应侧重读写</li>
<li>如果需要与人进行即时言语沟通，或想看“生肉”，或想听英文有声读物，应侧重听说</li>
</ul>
<p>今天主要是作为开篇，做个介绍，具体的方法，会在后续的内容中逐步展开。</p>
]]></content:encoded>
    </item>
    <item>
      <title>人人都能学会的英语2：音标</title>
      <link>https://levy.vip/english/everyone-can-learn-english-2-pronunciation.html</link>
      <guid>https://levy.vip/english/everyone-can-learn-english-2-pronunciation.html</guid>
      <source url="https://levy.vip/rss.xml">人人都能学会的英语2：音标</source>
      <description>人人都能学会的英语2：音标 方法 音标与发音是最基础也是最重要的环节，侧重听说的同学，一定要掌握好；侧重读写的人，也不能懈怠，因为这跟后面的单词学习有关系。 我推荐根据赖世雄的《美语音标》进行学习： 在微信公众号 常春藤英语集团&amp;nbsp;买相关书籍 在 喜马拉雅 或 B站 听相关音频，进行跟读练习 下载 谷歌翻译 app（或别的app），设置成&amp;nbsp;英 =&amp;gt;&amp;nbsp;中&amp;nbsp;翻译，检查自己发音正确，翻译软件能正确识别就算合格 有人可能会问，赖世雄是谁，没听说过啊。这是可以理解的，因为在进行这样的学习之前，我也没听过，但他却是中国十大名师之一。他最励志的经历是，一个在高考英语中只能得7分（满分100分）的人，经过学习，成为英语教学硕士。这是典型的后天成才的前辈，跟着他学，经过本人亲自验证，绝对靠谱。</description>
      <pubDate>Sun, 26 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 人人都能学会的英语2：音标</h1>
<h2> 方法</h2>
<p>音标与发音是最基础也是最重要的环节，侧重听说的同学，一定要掌握好；侧重读写的人，也不能懈怠，因为这跟后面的单词学习有关系。</p>
<p>我推荐根据赖世雄的《美语音标》进行学习：</p>
<ul>
<li>在微信公众号 常春藤英语集团&nbsp;买相关书籍</li>
<li>在 喜马拉雅 或 B站 听相关音频，进行跟读练习</li>
<li>下载 谷歌翻译 app（或别的app），设置成&nbsp;英 =&gt;&nbsp;中&nbsp;翻译，检查自己发音正确，翻译软件能正确识别就算合格</li>
</ul>
<p>有人可能会问，赖世雄是谁，没听说过啊。这是可以理解的，因为在进行这样的学习之前，我也没听过，但他却是中国十大名师之一。他最励志的经历是，一个在高考英语中只能得7分（满分100分）的人，经过学习，成为英语教学硕士。这是典型的后天成才的前辈，跟着他学，经过本人亲自验证，绝对靠谱。</p>
<p>我给出了我之前的学习进度记录，仅供参考<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426129817.png" alt="image.png"></p>
<p>学完之后，如果有兴趣，可以了解下进阶内容。American pronunciation workshop：<a href="https://www.bilibili.com/video/BV1qE411v7oE?vd_source=046b87a1b6f0cc325266cccbb4f307fa" target="_blank" rel="noopener noreferrer">https://www.bilibili.com/video/BV1qE411v7oE</a><br>
（我特意找了没有字幕的视频，建议就这样看。如果是侧重读写的同学，直接跳过就好）</p>
<p>再给出关于音标的一些资料：</p>
<ul>
<li><a href="http://yinbiao.tingclass.net/show-16-9-1.html" target="_blank" rel="noopener noreferrer">音标发音大全 48个英语音标表（IPA,DJ音标,KK音标对照表）</a></li>
<li><a href="https://new.qq.com/omn/20190106/20190106G062W8.html" target="_blank" rel="noopener noreferrer">一文理解IPA音标、DJ音标、KK音标的区别</a></li>
</ul>
<h2> 讨论</h2>
<p>这里我补充几点，虽然不是核心，却与主题息息相关：</p>
<ol>
<li>是否要买实体书？</li>
<li>为什么是美语，而不是“英语”</li>
<li>关于口音（accent）</li>
</ol>
<h3> 建议买实体书</h3>
<p>现在网络这么发达，信息随处可得，所以很可能你不愿意买实体书。</p>
<p>这样也行，但我仍然建议买书，反正我买了全套，因为我认为从某种意义上讲，实体书有助于提升行动力。还记得前面我们讲到过，整个学习过程强调的是实践，行动力或执行力是很重要的。而购买的动作，本身就是执行力的一种表现。</p>
<p>而相对的，白嫖的行为背后，隐含着不愿意付出的意味，这种思维本身是不可取的。当然，这也许是我过度解读，你可以继续白嫖。</p>
<p>不想买实体书的另一个原因是，书多了不方便，比如搬家很麻烦。对这个问题，我是这样处理的，仅供参考：</p>
<ul>
<li>捐书</li>
<li>送人</li>
</ul>
<h3> 美语是主流</h3>
<p>也许有人认为英音更好听，某些时候，我也赞同这个观点。但我们还是实用主义，现在西方世界谁说话最大声，大家都清楚。毕竟“日不落”已然衰落，现在还是美刀更好使（还在说刀的事，手动滑稽）！所以，美语才是主流。</p>
<p>一定要学英音的，建议通关后重修发音，或现在就另找门派学习。</p>
<h3> 不要纠结口音</h3>
<p>在国内的网络平台上，很常见的场景是一群人对某个中国人的口音评头论足，说什么“这个不正宗、不地道啊”，“你这个也不像美音呀”、”工地散装英语哈哈哈“。</p>
<p>我都不知道这些人在纠结什么。谁说话没口音呢？你作为中国人，能跟新闻联播主持人一样说普通话吗？你不也带点地方特色？就算是美国人，不同的地区，也有不同的口音，你说谁才是正宗的呢？</p>
<p>语言最重要的功能是沟通，能让别人听懂是首要需求——如果你的口音并不影响别人听懂并理解你，那就不用纠结它。这也是前面为什么我建议下载翻译软件的原因，让机器来判断你发音是否正确，免去担心自己口音的困扰。</p>
<p>总结一下，今天分享了音标学习的方法与经验，后面将分享单词的学习。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426129817.png" type="image/png"/>
    </item>
    <item>
      <title>人人都能学会的英语3：单词</title>
      <link>https://levy.vip/english/everyone-can-learn-english-3-words.html</link>
      <guid>https://levy.vip/english/everyone-can-learn-english-3-words.html</guid>
      <source url="https://levy.vip/rss.xml">人人都能学会的英语3：单词</source>
      <description>人人都能学会的英语3：单词 方法 单词是听说读写的基础，是有必要花时间去学习的。 然而，我不太喜欢使用“背单词”这样的描述，因为这会显得是在死记硬背、追求机械的记忆。对于单词的学习，我有以下建议： 请务必打好音标基础。一个单词如果你不会念、你听不出来，那就没有真正的掌握 结合上下文理解。一定要把单词放到句子里去学习，而不要只看英文单词选中文含义（或反之），因为英文单词与中文词汇并非一对一的关系，并且这样只是记忆，没有获得理解。 只推荐利用词根记忆（当然并非百分百有效） 学习软件：欧路词典——免费，全平台通用</description>
      <pubDate>Mon, 27 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 人人都能学会的英语3：单词</h1>
<h2> 方法</h2>
<p>单词是听说读写的基础，是有必要花时间去学习的。</p>
<p>然而，我不太喜欢使用“背单词”这样的描述，因为这会显得是在死记硬背、追求机械的记忆。对于单词的学习，我有以下建议：</p>
<ol>
<li>请务必打好音标基础。一个单词如果你不会念、你听不出来，那就没有真正的掌握</li>
<li>结合上下文理解。一定要把单词放到句子里去学习，而不要只看英文单词选中文含义（或反之），因为英文单词与中文词汇并非一对一的关系，并且这样只是记忆，没有获得理解。</li>
<li>只推荐利用词根记忆（当然并非百分百有效）</li>
<li>学习软件：欧路词典——免费，全平台通用</li>
</ol>
<p>以手机欧路词典为例，打开后，点击“学习”，选择适合自己的单词本（或先做测试让软件来推荐），进行学习即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426210376.png" alt="image.png"><br>
注意结合个人情况，进行学习设置，例如学习新词数量及、复习数量。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426214482.png" alt="image.png"><br>
就我个人而言，我的做法是把全部单词刷一遍，过滤掉我完全掌握的，然后再进行新词的复习。</p>
<h2> 提醒</h2>
<p>必须要给大家一个提醒，避免进入单词学习的陷阱：那就是忍受不了单词学习进度缓慢、记了又忘的情况，于是追求“超级记忆术”，把单词一股脑装在脑子里。</p>
<p>回顾前面所说，学习英语强调的是实践，而不是记忆。所以，不论记忆方法有多高明，它只能解决输入，不能解决输出的问题。因此，我们的重点应该放在输入与输出闭环上，而不是追求单方面“多快好省”。</p>
<p>网络平台上很多鼓吹类似于“一天背六千个单词”的营销内容，就是抓住了人们没有正确理解的情况下贪多求快的心理，一定要警惕。</p>
<p>再者，单词与阅读理解，并非简单的线性对应关系，即不是单词量学得越多，阅读理解越深。另一方面来说，也不是只有单词量“爆表”，才能进行全英文阅读。</p>
<p>以我自身为例，我可以阅读原著，但我的词汇量不过四千左右。</p>
<p>我之前在某个词汇量评测网站做过评估：<a href="http://testyourvocab.com/result?user=13229458" target="_blank" rel="noopener noreferrer">http://testyourvocab.com/result?user=13229458</a><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426218162.png" alt="image.png"></p>
<p>4360 是网站为我估算的单词数量。</p>
<p>为求精确，我还找了一本考研（包含了高考、四级、六级、考研词汇）的单词本，把上面的所有单词刷了一遍，结果如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426221966.png" alt="image.png"><br>
上图的含义为：</p>
<ul>
<li>0 个未学习，意思是单词本的所有单词我都遇到了一遍了，并且我都进行了掌握程度判断</li>
<li>已掌握 3844，意是这些单词我一看就能知道是什么意思，并且能做翻译</li>
<li>学习中 4122，具体又可细分为：
<ul>
<li>认识：一词多义，仅知道部分的中文含义，当其在另一个句子中体现另一种意思时，自己不知道</li>
<li>模糊：原先不知道中文含义，但根据例句能隐约推断出其含义</li>
<li>不认识：给了例句也看不懂</li>
</ul>
</li>
</ul>
<p>从上面我对学习中的单词的分类就可以看出，单词这件事，不是非黑即白的。而把单词掌握数量直接与英语能力挂钩，也是不可取的。</p>
<p>我在学生时代，其实没有买过一本单词本，也没有刻意地去背过单词（例如每天固定背50个），所以上述词汇量，还是挺客观的。就以词汇量而言，可能很多人都比我强——既然单词对我来说不是困扰，那对大家来说，应该也不是。我想这点信心，大家是可以有的。</p>
<p>总之，我想说的是，单词重要，却也不用刻意强调“背”这件事。把单词与英语能力直接挂钩，追求神奇的背单词的方法，这是很多人学英语的误区。而在这种误区下，就催生了速成的方法论，迎合了浮躁社会下急功近利的心态。但其实，强调实践，讲究输入输出闭环，才是更符合客观规律的学习方式。</p>
<p>记住：重要的不是你记住了多少单词，而是你能运用多少单词来造句。</p>
<p>延伸阅读：<a href="/english/learning-7000-words-task-completed.html" target="blank">刷7000个单词的经验总结</a>。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426210376.png" type="image/png"/>
    </item>
    <item>
      <title>人人都能学会的英语4：听说</title>
      <link>https://levy.vip/english/everyone-can-learn-english-4-listening-and-speaking.html</link>
      <guid>https://levy.vip/english/everyone-can-learn-english-4-listening-and-speaking.html</guid>
      <source url="https://levy.vip/rss.xml">人人都能学会的英语4：听说</source>
      <description>人人都能学会的英语4：听说 前言 听说篇相对独立，与读写篇没有依赖关系，但需要确保前面的基础已打好。 方法 听说属于综合能力，建议遵循以下学习步骤： 激发兴趣，建立信心 明确方向 脚踏实地，坚持输入并输出 1.建立信心 首先找到有趣的音视频材料，利用生动的内容增加学习的动力。同时应注意材料的难度不应过高，有助于自己建立信心——已经很有自信的同学可跳过此步骤。我推荐以下材料： Journey to the West： https://b23.tv/61xkAgc English at Work：https://www.bbc.co.uk/learningenglish/english/features/english-at-work</description>
      <pubDate>Tue, 28 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 人人都能学会的英语4：听说</h1>
<h2> 前言</h2>
<p>听说篇相对独立，与读写篇没有依赖关系，但需要确保前面的基础已打好。</p>
<h2> 方法</h2>
<p>听说属于综合能力，建议遵循以下学习步骤：</p>
<ol>
<li>激发兴趣，建立信心</li>
<li>明确方向</li>
<li>脚踏实地，坚持输入并输出</li>
</ol>
<h3> 1.建立信心</h3>
<p>首先找到有趣的音视频材料，利用生动的内容增加学习的动力。同时应注意材料的难度不应过高，有助于自己建立信心——已经很有自信的同学可跳过此步骤。我推荐以下材料：</p>
<ul>
<li>Journey to the West： <a href="https://b23.tv/61xkAgc" target="_blank" rel="noopener noreferrer">https://b23.tv/61xkAgc</a></li>
<li>English at Work：<a href="https://www.bbc.co.uk/learningenglish/english/features/english-at-work" target="_blank" rel="noopener noreferrer">https://www.bbc.co.uk/learningenglish/english/features/english-at-work</a></li>
</ul>
<p>前者是西游记英文动画版，后者是BBC的职场英语动画片，任选其一即可（如果是实用主义，当然选第二个啦）。<br>
两个材料都是只有英文字幕的，要逐渐习惯这种模式。这就是为什么说学习时要注重心理建设，因为需要走出舒适区。</p>
<p>记住，上面给的材料，主要是帮助自己建立自信的， 学到什么不重要，重要的是你觉得自己行了，可以进行下一步了。</p>
<h3> 2.明确方向</h3>
<p>在进行深入学习前，我们需要明确自己的学习方向，究竟是 Daily English 还是 Business English?</p>
<p>当然可以全部都学，但那是结果，在过程中总要分个前后。对这个问题的回答，取决于读者自己。</p>
<p>Daily English 我推荐：<a href="https://www.youtube.com/c/EnglishSpeakingCourses/search?query=conversation" target="_blank" rel="noopener noreferrer">https://www.youtube.com/c/EnglishSpeakingCourses/search?query=conversation</a><br>
这个视频如果看不了，可以看我写的教程：<a href="https://www.yuque.com/levy/blog/how-to-surf" target="_blank" rel="noopener noreferrer">https://www.yuque.com/levy/blog/how-to-surf</a></p>
<p>Business English，我推荐 eslpod 的教材：</p>
<ol>
<li><a href="https://secure3.eslpod.com/product/using-english-at-work/" target="_blank" rel="noopener noreferrer">Using English at Work</a></li>
<li><a href="https://secure3.eslpod.com/product/english-for-business-meetings/" target="_blank" rel="noopener noreferrer">English for Business Meetings</a></li>
<li><a href="https://secure3.eslpod.com/product/interview-questions-answered/" target="_blank" rel="noopener noreferrer">Interview Questions Answered</a></li>
</ol>
<p>这里特别说下，eslpod 的创始人是英语方面的语言学及教育学的博士，可以说没有人比他更懂英语教学（手动滑稽）！跟着他学，我成功地喜欢上了美语，从此以后听加州口音会觉得最舒服。</p>
<p>以上三本教材我自己买了，有需要的可以私信我发你。</p>
<p>另外，赖世雄的《高级美语》也极力推荐，它是属于综合性的教材，内容方面起到开阔视野的作用。而我推荐它的原因是，本书北美外教的发音尤为悦耳，朗读课文时简直“说得比唱得还好听”，练习听说的同学一定不要错过。</p>
<h3> 3.坚持输入并输出</h3>
<p>有了材料还远远不够，只是在听、光在那里看，结果还是哑巴英语。因此，对于无论什么材料，我建议这样使用：</p>
<ol>
<li>不看课本（字幕），全部语音听完</li>
<li>看课本（字幕），做笔记，学习单词与短语</li>
<li>慢速跟读
<ol>
<li>看课本（字幕），播一句，暂停，跟读一句</li>
<li>不看课本（字幕），播一句，暂停，跟读一句</li>
<li>看课本（字幕），不暂停，课程跟读</li>
</ol>
</li>
<li>独自念出来，并使用手机软件（如微信）录音，听一听自己说的效果，并进行语音翻译，以检测自己的输出正确率</li>
<li>脱稿表达（此步骤可选）</li>
</ol>
<p>在这里分享一个我遇到的“困境”。在进行不看课本的慢速跟读的练习时，播一句跟读一句的形式，让我很容易犯困。但我没有因此就放弃，也没想办法强行提神，而是转而把它当作催眠工具——困了就睡觉。这样过了两周，有一天晚上，我没有犯困，做完了训练。而那一天也是我走出“困境”的日子，后面的日子里，再也没犯困过！</p>
<p>其实我不止有一次类似的“犯困”经历，都是与学习新东西有关的。我想，这可能是大脑在生理上对学习的抗拒。对于新领域的知识或不同寻常的学习方式，是需要走出舒适区的，而这种“犯困”可能是大脑潜意识在拒绝走出舒适区。对此，应对方法很简单，那就是坚持下去，每天固定时间执行相应的学习动作，直到大脑潜意识“屈服”，转而适应新的东西。</p>
<p>以上就是今天要分享的关于听说练习的内容，下一期将分享英语读写的学习经验。</p>
]]></content:encoded>
    </item>
    <item>
      <title>人人都能学会的英语5：读写</title>
      <link>https://levy.vip/english/everyone-can-learn-english-5-reading-and-writing.html</link>
      <guid>https://levy.vip/english/everyone-can-learn-english-5-reading-and-writing.html</guid>
      <source url="https://levy.vip/rss.xml">人人都能学会的英语5：读写</source>
      <description>人人都能学会的英语5：读写 前言 读写篇相对独立，与听说篇没有依赖关系，但需要确保前面的基础已打好。 阅读能力升级之旅 我先给出自己经历过的全英阅读能力的变化过程，仅供参考： 看到英文网站，第一反应是点击切换中文版 一些技术资料、技术文章，往往是英文，没有翻译，只能硬着头皮借助浏览器的翻译或词典进行全英阅读 适应了阅读英文技术文档、英文技术博客，但看非技术领域的内容，如在 wikipedia 上查某个人物、某部电影，第一反应是切换中文版（此时对英文的阅读只停留在自己的专业领域） 尝试阅读全英书籍。其实看得明白，但总觉得太慢了、是不是在浪费时间、好想看中文版，需要耐着性子阅读 阅读量到达一定后，开始对内容挑剔，如认为议论文没有生动的例子不吸引人，或内容充满说教不想看，于是开始学会挑重点，跳过不重要或没兴趣的内容，不再逐字逐句阅读 阅读英文在感观上跟阅读中文没有重大的区别，也即看到英文内容，不会再想“这是英文，不是中文哦”——翻阅内容，看就完事了</description>
      <pubDate>Wed, 29 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 人人都能学会的英语5：读写</h1>
<h2> 前言</h2>
<p>读写篇相对独立，与听说篇没有依赖关系，但需要确保前面的基础已打好。</p>
<h2> 阅读能力升级之旅</h2>
<p>我先给出自己经历过的全英阅读能力的变化过程，仅供参考：</p>
<ol>
<li>看到英文网站，第一反应是点击切换中文版</li>
<li>一些技术资料、技术文章，往往是英文，没有翻译，只能硬着头皮借助浏览器的翻译或词典进行全英阅读</li>
<li>适应了阅读英文技术文档、英文技术博客，但看非技术领域的内容，如在 wikipedia 上查某个人物、某部电影，第一反应是切换中文版（此时对英文的阅读只停留在自己的专业领域）</li>
<li>尝试阅读全英书籍。其实看得明白，但总觉得太慢了、是不是在浪费时间、好想看中文版，需要耐着性子阅读</li>
<li>阅读量到达一定后，开始对内容挑剔，如认为议论文没有生动的例子不吸引人，或内容充满说教不想看，于是开始学会挑重点，跳过不重要或没兴趣的内容，不再逐字逐句阅读</li>
<li>阅读英文在感观上跟阅读中文没有重大的区别，也即看到英文内容，不会再想“这是英文，不是中文哦”——翻阅内容，看就完事了</li>
</ol>
<p>这是怎么做到的呢？我主要采用了以下方法。</p>
<h2> 阅读方法</h2>
<h3> 建立信心</h3>
<p>如果之前读者并没有全英文的阅读体验，建议先根据兴趣选一本书（或阅读材料）后，在两周内读完它——无论用什么手段，一定要在该时间段内完成。此举似小实大，有两点内涵：</p>
<ul>
<li>时长再拉长，你很可能读了后面忘前面；时间越长，越拖延，则两相结合，很可能最终放弃了阅读</li>
<li>在较短的时间内读完，能快速建立信心，为后续阅读打下基础</li>
</ul>
<p>第一本书或阅读材料很关键，感兴趣是最重要的，当然也可以结合后面提到的蓝思值进行选材。</p>
<p>我当初使用的是 轻听英语 app，在上面看完了英文文字加语音版的死亡笔记。看完后感受如下：</p>
<ul>
<li>一开始的阅读速度比音频的播放速度还要慢，如果没有兴趣，很可能就没耐心了</li>
<li>因为是动漫的文字版，除了画外音（内心的独白），几乎全是人物的对话，这些内容是很容易理解的</li>
</ul>
<p>之后我信心大涨，用一周时间，把魔兽世界官方小说《Arthas: Rise of the Lich King》原版看完了。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427477232.png" alt="image.png"></p>
<h3> 根据蓝思值选书</h3>
<p>有了信心之后，我们就可以科学地、有节奏地培养自己的英文阅读能力了。</p>
<p>核心思路是评估自己的阅读<a href="https://lexile.com/parents-students/understanding-your-lexile-measure/lexile-measures-reading/" target="_blank" rel="noopener noreferrer">蓝思值</a>，再找到适合自己的阅读材料。什么是蓝思值呢？<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427484594.png" alt="image.png"><br>
假设你的阅读水平是 600L，那么蓝思值在 550L~700L 的阅读材料比较适合你。再低就太简单，达不到提升的效果；再高就超出阅读能力太多，很可能读不下去（因为我们在讲全英文阅读，所以我就不翻译了）。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427489979.png" alt="image.png"></p>
<p>综上所述，推荐按以下步骤提升阅读能力：</p>
<ol>
<li>评估自己的阅读能力 <a href="https://readtheory.org/" target="_blank" rel="noopener noreferrer">https://readtheory.org/</a> <img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427496404.png" alt="image.png"></li>
<li>找到自己感兴趣的读物，通过网站判定书籍的蓝思值是否合适 <a href="https://hub.lexile.com/find-a-book/book-results" target="_blank" rel="noopener noreferrer">https://hub.lexile.com/find-a-book/book-results</a></li>
</ol>
<p>相关的 app 我推荐: 轻听英语，里面的书都是免费的。当然大家也可以推荐别的 app，直接有标明蓝思值的就最好不过了。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427501518.png" alt="lADPJw1WS0BSWVnNBQDNBMw_1228_1280.jpg"><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1656413042641-47c45b34-b4d2-4197-9af8-536ea2e8b4be.jpeg" alt=""></p>
<h3> 巧查生词</h3>
<p>阅读的一个拦路虎是生词。大家的第一反应是认为生词太多，看不懂，很容易放弃。</p>
<p>首先，我推荐欧路词典。如果是在电脑上看 pdf，鼠标选中区域就能查词或翻译，非常方便。</p>
<p>再者，表达一个观点：有些单词不认识，甚至有些句子无法理解，并不影响对情节的核心内容的理解。事实上，除去欧亨利式结尾的短篇小说，书的内容越多，其核心内容越不容易受生词影响。因为主题是贯穿全文的，前面因为生词没看懂，后文换了个说法，你就能看得懂了！</p>
<p>因此，对生词，我建议：一页只查一个生词。它的意义是：</p>
<ol>
<li>你不会因为频繁查生词而影响阅读体验。事实上，试图把每一个生词都查一遍，是阅读不下去的主要原因！</li>
<li>因为查词机会的“稀缺性”，你把机会留给你认为最有价值的生词</li>
<li>强迫你的大脑思考，如何在带着许多“迷雾”的情况下，去理解句子、段落，抓住故事的核心情节，理解书籍的中心思想</li>
</ol>
<p>在阅读《Arthas: Rise of the Lich King》的时候，我的体验就是：</p>
<ul>
<li>有些形容词或副词不认识，并不影响理解句子的大意</li>
<li>阅读几章后，为加快阅读速度，只出现一次的生词，我不再查字典——我只查出现过至少两次的单词</li>
<li>再看到后面，有些单词不认识，我都懒得查了，反正不影响我对整体剧情的理解</li>
</ul>
<h3> 阅读材料推荐</h3>
<p>如果实在找不到全英阅读材料，我推荐看《现代大学英语精读(第2版）》, 我已经挑选了值得一读的文章，英语高考有120分或过了四级的人，应该可以直接<a href="/english/contemporary-college-english-1.html" target="blank">点击查看</a>。</p>
<h2> 写作</h2>
<p>阅读是输入，写作是输出，二者是相辅相成的。 对于写的能力，推荐从读书笔记开始做练习 （就像看完中文书那样做笔记），也可以找机会与人发邮件。</p>
<p>读书笔记示例：</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/indispensable-opposition.png" alt="indispensable-opposition.png" tabindex="0"><figcaption>indispensable-opposition.png</figcaption></figure>
<p>这是我购买 <a href="http://eslpod.com" target="_blank" rel="noopener noreferrer">eslpod.com</a> 教材时发的邮件：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427508571.png" alt="image.png"></p>
<p>这是我申请 JetBrains 正版授权时发的邮件：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427519794.png" alt="image.png"></p>
<p>还有一种方法， 那就是利用软件的翻译功能，把自己写的中文笔记，翻译成英文，再去校对、修改。很多文本类 Web 应用都有此功能，可以利用起来。<br>
这种方法本质就是，自己一开始不知道英文文章写什么，那就从校对、修改开始做起；自己不能写完整的英文文章，那就从摘取段落开始做起。适应这个过程后，写作的障碍就减少了。</p>
<p>总结一下，英文读写的提升需要踏出舒适区，要耐着性子，不要怕慢。坚持去做，量变终会有质变！</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682427477232.png" type="image/png"/>
    </item>
    <item>
      <title>英文能力评测手把手教学</title>
      <link>https://levy.vip/english/how-to-self-evaluate-english-level.html</link>
      <guid>https://levy.vip/english/how-to-self-evaluate-english-level.html</guid>
      <source url="https://levy.vip/rss.xml">英文能力评测手把手教学</source>
      <description>英文能力评测手把手教学 前言 之前有提到过两个英文能力评测的网站，分别用于评估蓝思值与单词量。实践中我发现有些细节可能会被大家忽略，故写下此文，以作手把手教学之用。 单词量 进入网站：https://preply.com/en/learn/english/test-your-vocab 看到如图所示区域，开始勾选单词，规则很简单：一眼看上去认识的就勾选，不认识、犹豫的都跳过，之后点击红框处继续。注意，千万不要勾选的过程中去查单词。 在下一页，重复上面的步骤，之后点击 Continue，等待几秒钟，就会出现评估的单词量。 对于该结果，网站有以下值得注意的解释：</description>
      <pubDate>Sat, 09 Jul 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 英文能力评测手把手教学</h1>
<h2> 前言</h2>
<p>之前有提到过两个英文能力评测的网站，分别用于评估蓝思值与单词量。实践中我发现有些细节可能会被大家忽略，故写下此文，以作手把手教学之用。</p>
<h2> 单词量</h2>
<p>进入网站：<a href="https://preply.com/en/learn/english/test-your-vocab" target="_blank" rel="noopener noreferrer">https://preply.com/en/learn/english/test-your-vocab</a></p>
<p>看到如图所示区域，开始勾选单词，规则很简单：一眼看上去认识的就勾选，不认识、犹豫的都跳过，之后点击红框处继续。注意，千万不要勾选的过程中去查单词。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426636662.png" alt="image.png"><br>
在下一页，重复上面的步骤，之后点击 <code>Continue</code>，等待几秒钟，就会出现评估的单词量。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426647325.png" alt="image.png"><br>
对于该结果，网站有以下值得注意的解释：</p>
<ol>
<li>结果偏差在正负10%左右：</li>
</ol>
<blockquote>
<p>Your vocabulary count has a margin of error of approximately ±10%. We also round results above 10,000 to the nearest 100, and results above 300 to the nearest 10.</p>
</blockquote>
<ol start="2">
<li>英语非母语者，用单词量作为能力标准通常划分如下：</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426651054.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<ol start="3">
<li>结果一年测一次最有效</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426657822.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<h2> 阅读能力</h2>
<p>评估阅读能力也就是评估蓝思值。进入网站：<a href="https://readtheory.org/" target="_blank" rel="noopener noreferrer">https://readtheory.org/</a></p>
<p>右上角点击注册：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426663590.png" alt="image.png"></p>
<p>选择作为学生：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426667189.png" alt="image.png"></p>
<p>可以直接使用谷歌账号注册：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426672418.png" alt="image.png"><br>
如果不能谷歌，可以看我写的谷歌教程：<a href="https://www.yuque.com/levy/blog/how-to-surf" target="_blank" rel="noopener noreferrer">https://www.yuque.com/levy/blog/how-to-surf</a><br>
或者自己填表单，进入下一步。</p>
<p>确保自己至少有20分钟的空闲时间，之后点击已经准备好了：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426676249.png" alt="image.png"></p>
<p>开始做题，一共8道，看不懂就乱选，后面的题目会根据你的答题情况自动调整：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426719299.png" alt="image.png"></p>
<p>做完后，点击确认：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426727690.png" alt="image.png"></p>
<p>点击下图红框处：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426733660.png" alt="image.png"></p>
<p>往下翻阅，即可看到自己的蓝思值初步评分：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426739821.png" alt="image.png"></p>
<p>再给出通用的蓝思值能力对照表：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426744993.png" alt="image.png"><br>
以上就是今天的英文能力评测手把手教学啦，希望对大家有所帮助。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426636662.png" type="image/png"/>
    </item>
    <item>
      <title>完成刷7k单词任务</title>
      <link>https://levy.vip/english/learning-7000-words-task-completed.html</link>
      <guid>https://levy.vip/english/learning-7000-words-task-completed.html</guid>
      <source url="https://levy.vip/rss.xml">完成刷7k单词任务</source>
      <description>完成刷7k单词任务 这周算是完成了今年定下的刷单词的任务，写篇文章总结下，为这件事划上一个句号，也给有需要的人一定的参考价值。 首先，解释下为什么完成度不是100%，我也称之为完成任务。因为软件出问题了（又或是我使用方式问题），剩下的单词无法进行学习了，进度条止步于此。我也不想纠结于这一点，就这样吧，算是残缺美。 再者，说明下为什么要做这件事。主要原因有三：</description>
      <pubDate>Sat, 17 Sep 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 完成刷7k单词任务</h1>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426608041.png" alt="image.png"><br>
这周算是完成了今年定下的刷单词的任务，写篇文章总结下，为这件事划上一个句号，也给有需要的人一定的参考价值。</p>
<p>首先，解释下为什么完成度不是100%，我也称之为完成任务。因为软件出问题了（又或是我使用方式问题），剩下的单词无法进行学习了，进度条止步于此。我也不想纠结于这一点，就这样吧，算是残缺美。</p>
<p>再者，说明下为什么要做这件事。主要原因有三：</p>
<ol>
<li>主动扩充词汇量，与英文阅读相辅相成。诚然，可以一边阅读一边积累词汇，但这种方式我认为是“被动”的，并且有看我之前英文阅读经验分享的朋友，会知道我阅读时查词的频率很低的，如此增加词汇量的效率太慢。另外，词汇又有高频与低频、重点与非重点之分，而这是单词本的强项，因此利用单词本是接触“精华词汇”的高效方式。</li>
<li>我想知道自己确切的词汇量。虽然在之前分享的在线工具中，我评测过自己的词汇量（7000），但自己感觉有点“虚”，一方面误差有10%，另一方面我觉得波动太大了（两年前测是4000），不太相信，于是打算用刷单词本的“笨方法”去确认自己的词汇量。</li>
<li>我想体验一下刷单词本的经历。这点我在之前的文章中也透露过，我在学生时代是没有买过单词本、也没有拿着单词本去背诵过，基本都是靠音标+词根，边学课文时边记忆，讲究的是一个自然，没有刻意。然而，这隐约有种“不踏实”之感，没有经过刻意练习，似乎得来的单词“不会珍惜”，于是便趁这个机会把这个经历补上。</li>
</ol>
<p>最后，我想强调的是，注意我“刷单词”，而不是“背单词”这种描述上的差别。“刷”重在强调，过一遍，有个印象、能认识；“背”强调的是死记硬背，机械地记忆——虽然我认为练习需要刻意，但却不认为它等同于死记。</p>
<p>很多人在学习单词觉得很痛苦，半途而废，很大的原因是强调背，把内容强塞进脑子里很让人痛苦；而背了又忘，更是让人感到强烈的挫败感。在这里，我想分享的经验是，此时需要进行思维的转换：你允许自己忘记某些单词。可以把新单词想像成陌生人，总有人能与你成为好友，总有人不能给你留下深刻印象，总有人与你相处不愉快——这么多单词，你先认识容易认识的，很难“相处”、容易忘记的单词，后面再说呗，没必要一口气吃成胖子。</p>
<p>为了减少痛苦，我有以下经验，它们是属于启发式的（heuristic），仅供参考。每个人应总结符合自己的经验，其核心思想是分层次、分类，不要一刀切。</p>
<p>对单词的掌握程度区分优先级：</p>
<ul>
<li>读：阅读时能看懂是什么意思；就算第一眼看不出单词意思，结合上下文能推测出意思才行</li>
<li>听：别人正常语速、慢速说时，自己能听出这是哪个单词</li>
<li>说：我不对此有强制要求，看着音标能发音即可，因为当前我的英语实践场景中，并不侧重说</li>
<li>写：此优先级最低，因为写作不是考试，我完全可以查词典，因此有些词很难记我就索性不记，如官僚主义（bureaucratism）、资产阶级（bourgeois）——我能听、阅读，但放弃掌握写</li>
</ul>
<p>对词意进行分类掌握：有的词十几个意思，你哪能一下子全记住？</p>
<ul>
<li>结合相应的例句，一次只记一个意思</li>
<li>常见的词意，优先记住</li>
<li>冷门的词意，可以跳过</li>
<li>另外，有些中文词意解释很牵强，可以找英英释义</li>
</ul>
<p>对词汇进行分类掌握：识别出单词属于哪个领域的</p>
<ul>
<li>计算机、法律、经济、日常生活、文学等领域的词汇，我放在较高优先级，尽量去掌握</li>
<li>医学领域、非考试重点词汇，我不太重视，能认识就认识，觉得认识上有困难就跳过</li>
</ul>
<p>当然，单词不能纯刷，要结合阅读一起来做。我选择的是外国语学院的英语专业教材《现代大学英语精读》，二者结合，相辅相成。</p>
<p>还有就是，最好固定时间段，以便形成习惯，如上班坐地铁，或晚饭后等较规律的时间段。</p>
<p>最后，我想说的是，刷了这本单词本，并不意味着我就 master 7,000 English words。它对我而言，更多的是在英语学习经历上，具有里程碑的意义。回顾一下，从2022年2月份开始到，到现在历经大约7个月，我算是把英语单词这件事的“遗憾”给补上了，此事至此告一段落，我可以开始新的旅程了🎉。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1682426608041.png" type="image/png"/>
    </item>
    <item>
      <title>让 ChatGPT 成为你的外语私教</title>
      <link>https://levy.vip/english/let-chatgpt-be-your-foreign-language-teacher.html</link>
      <guid>https://levy.vip/english/let-chatgpt-be-your-foreign-language-teacher.html</guid>
      <source url="https://levy.vip/rss.xml">让 ChatGPT 成为你的外语私教</source>
      <description>让 ChatGPT 成为你的外语私教 前言 有了 ChatGPT 后，练习外语口语的门槛再次降低，没有外语环境再也不是问题了——AI 就是你的专属私教。 本文将分享借助 AI 进行口语练习的一些工具、方法与实践经验，仅供参考。 准备工作 在开始之前，要准备好几样东西： ChatGPT, 如果没有账号或不能上网，请查看上网教程 Chrome 浏览器插件 voice-control-for-chatgpt 口语练习题，根据个人需求查找即可，下文将以雅思为例进行说明</description>
      <pubDate>Sat, 06 May 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 让 ChatGPT 成为你的外语私教</h1>
<h2> 前言</h2>
<p>有了 ChatGPT 后，练习外语口语的门槛再次降低，没有外语环境再也不是问题了——AI 就是你的专属私教。</p>
<p>本文将分享借助 AI 进行口语练习的一些工具、方法与实践经验，仅供参考。</p>
<h2> 准备工作</h2>
<p>在开始之前，要准备好几样东西：</p>
<ol start="0">
<li><a href="https://chat.openai.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a>, 如果没有账号或不能上网，请查看<a href="/tools/how-to-connect-to-internet.html" target="blank">上网教程</a></li>
<li><a href="https://chrome.google.com/webstore/detail/voice-control-for-chatgpt/eollffkcakegifhacjnlnegohfdlidhn" target="_blank" rel="noopener noreferrer">Chrome 浏览器插件 voice-control-for-chatgpt</a></li>
<li>口语练习题，根据个人需求查找即可，下文将以<a href="https://liuxue.koolearn.com/ielts/speak-1-44-0/" target="_blank" rel="noopener noreferrer">雅思</a>为例进行说明</li>
</ol>
<p>安装好插件后，打开 chatGPT 界面，下方就会出现语音输入按钮。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211581972.png" alt="image.png"></p>
<h2> 常用Prompt</h2>
<p>下面总结了常用的 Prompt，可以有根据需要进行使用或调整。</p>
<p>设置角色：</p>
<ol>
<li>Please act as an English teacher.</li>
<li>Please act as an English-speaking test examiner.</li>
<li>Please act as IELTS speaking test examiner.</li>
</ol>
<p>进入一问一答模式：</p>
<ol>
<li>You're supposed to asked me questions and wait for my answer. The next question is: xxx</li>
</ol>
<p>对回答进行完善：</p>
<ol>
<li>Please revise my answer</li>
<li>Please modify my answer to make it more fluent</li>
</ol>
<p>对回答进行评分：</p>
<ol>
<li>Please rate my answer</li>
</ol>
<h2> 进行对话</h2>
<p>第一句话，是设置好 AI 的角色，让它扮演口语考官。</p>
<p>可以使用以下 prompt:</p>
<div class="language-markdown line-numbers-mode" data-ext="md"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211593449.png" alt="image.png"><br>
可以看出，语音转文字出现错误，单词 IELTS 始终未能正确识别，但 ChatGPT 却能明白其中的意思。</p>
<p>开启语音插件的意义在于，如果语音识别不了自己说的话，很有可能是自己的发音有问题，起到提醒自己纠正发音的作用。另外，ChatGPT 回复的文字，也会转换成语音输出，顺便练习了听力。</p>
<p>根据练习材料，让 AI 问自己问题。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211601688.png" alt="image.png"><br>
记得让 AI 对自己的回答评分，可以使用以下 prompt：</p>
<div class="language-markdown line-numbers-mode" data-ext="md"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211608559.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<p>进行下一个问题：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211615610.png" alt="image.png"><br>
上述回答不太好，AI 给出了理由：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211861818.png" alt="image.png"></p>
<p>修改后再回答，有所进步<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211906368.png" alt="image.png"></p>
<p>再问下一个问题：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211920743.png" alt="image.png"><br>
这个回答同样不理想，但看了提示也不知道要怎么改：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211928334.png" alt="image.png"></p>
<p>此时可以新建一个聊天窗口，让 AI 提供示例回答：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211938210.png" alt="image.png"></p>
<p>根据示例答案，结合关键词，重新组织语言，切换回原聊天窗口，再回答一次：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211955902.png" alt="image.png"><br>
有所进步！</p>
<p>有时对话长了，AI 会“糊涂”如下所示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683373370527.png" alt="image.png"></p>
<p>此时要重新强调它扮演的角色，让其回忆起上下文，可以使用以下 prompt:</p>
<div class="language-markdown line-numbers-mode" data-ext="md"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683373376359.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<p>除此之外，就没啥值得注意的了。重复上述过程，不断练习即可。</p>
<h2> 记录回答</h2>
<p>做完了练习，还要作笔记。但在记录回答之前，还要润色一下，毕竟口语表达的时候，可能会存在语法错误。</p>
<p>进入 <a href="https://quillbot.com/" target="_blank" rel="noopener noreferrer">https://quillbot.com/</a>，把答案复制上去，先进行语法检查：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211964246.png" alt="image.png"><br>
再进行流畅度润色：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211997332.png" alt="image.png"><br>
最后保存到笔记本上即可。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/english/1683211581972.png" type="image/png"/>
    </item>
    <item>
      <title>关于 Arm 你需要了解的三件事</title>
      <link>https://levy.vip/devops/about-arm-things-you-need-to-know.html</link>
      <guid>https://levy.vip/devops/about-arm-things-you-need-to-know.html</guid>
      <source url="https://levy.vip/rss.xml">关于 Arm 你需要了解的三件事</source>
      <description>关于 Arm 你需要了解的三件事 Arm 是另一种CPU架构（CISC），与常见的 x86 有所不同（RISC）。 跟我们有什么关系呢？ MacOS 的 M1 芯片是基于 Arm 的 云厂商及生态都在积极与 Arm 进行合作 Docker 镜像的构建有注意事项 构建镜像时，为 Arm 平台构建镜像时，常见的问题：exec user process caused: exec format error。 这是因为试图在 x86 机器上执行对平台有依赖的命令，如 shell 命令。</description>
      <pubDate>Wed, 02 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 关于 Arm 你需要了解的三件事</h1>
<p>Arm 是另一种CPU架构（CISC），与常见的 x86 有所不同（RISC）。</p>
<p>跟我们有什么关系呢？</p>
<ol>
<li>MacOS 的 M1 芯片是基于 Arm 的</li>
<li>云厂商及生态都在积极与 Arm 进行合作</li>
<li>Docker 镜像的构建有注意事项</li>
</ol>
<p>构建镜像时，为 Arm 平台构建镜像时，常见的问题：<code>exec user process caused: exec format error</code>。<br>
这是因为试图在 x86 机器上执行对平台有依赖的命令，如 shell 命令。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1690980143888-a9ffdab2-8be1-4463-84ef-d83be1c6d6c5.png" alt=""></p>
<p>解决办法就是，想办法把相关命令前置，提前执行，再构建镜像。</p>
<p>如注释掉 Dockerfile 里的　<code>Run chmod 777</code>，改成在构建镜像前执行。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/2023-12-17-zfOoTC.png" alt=""></p>
<p>视频里有更详细的讲解：</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1690980143888-a9ffdab2-8be1-4463-84ef-d83be1c6d6c5.png" type="image/png"/>
    </item>
    <item>
      <title>对象存储静态资源常见操作</title>
      <link>https://levy.vip/devops/common-solutions-of-object-storage-for-static-assets.html</link>
      <guid>https://levy.vip/devops/common-solutions-of-object-storage-for-static-assets.html</guid>
      <source url="https://levy.vip/rss.xml">对象存储静态资源常见操作</source>
      <description>对象存储静态资源常见操作 前言 把静态资源放到云厂商的对象存储服务中托管是很常见的实践，但由于涉及的事项较多，故记录下来，方便查阅。 本文主要以阿里云OSS的控制台界面作为操作示例，其逻辑同样适用于华为云OBS、Amazon S3，只是可能界面上有差异，具体需要看相关的官方文档。</description>
      <pubDate>Fri, 05 Apr 2019 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 对象存储静态资源常见操作</h1>
<h2> 前言</h2>
<p>把静态资源放到云厂商的对象存储服务中托管是很常见的实践，但由于涉及的事项较多，故记录下来，方便查阅。</p>
<p>本文主要以阿里云OSS的控制台界面作为操作示例，其逻辑同样适用于华为云OBS、Amazon S3，只是可能界面上有差异，具体需要看相关的官方文档。</p>
<!-- more -->
<h2> 阿里云OSS</h2>
<p>对于新建的bucket，需要做一些设置，才能正常使用静态资源。</p>
<h3> 绑定域名</h3>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702347742834-834879a6-3610-4b57-b0d3-73c7bcdead4e.png#averageHue=%23eaeae6&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=342&amp;id=u96fb23ed&amp;originHeight=342&amp;originWidth=1808&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=302360&amp;status=done&amp;style=none&amp;taskId=u8ae79e13-fed5-4ff3-9214-5f220758b8e&amp;title=&amp;width=1808" alt="" tabindex="0"><figcaption></figcaption></figure>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348155056-40bd48ea-53f8-48c1-88f4-82cc11460d80.png#averageHue=%23fdfdfd&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=421&amp;id=ubf7c51d8&amp;originHeight=421&amp;originWidth=1480&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=204064&amp;status=done&amp;style=none&amp;taskId=ua9e752a5-6884-4531-8e86-ab7cad1773f&amp;title=&amp;width=1480" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>则使用自定义域名访问，可以解决访问 html 变成下载的问题。</p>
<h3> CNAME设置</h3>
<p>如果绑定的是同一个阿里云账号下的域名，则可以自动添加 CNAME 记录。否则需要手动添加。</p>
<p>查看 bucket 外网地址：<a href="http://my-bucket.oss-cn-shenzhen.aliyuncs.com" target="_blank" rel="noopener noreferrer">my-bucket.oss-cn-shenzhen.aliyuncs.com</a><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348349732-a5a3015a-262a-4b32-b89d-593c32e56cfc.png#averageHue=%23faf9f9&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=662&amp;id=u278acbcc&amp;originHeight=662&amp;originWidth=1590&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=226370&amp;status=done&amp;style=none&amp;taskId=u455741d8-010a-4202-9b96-170a9d9ffb7&amp;title=&amp;width=1590" alt=""></p>
<p>则去域名解析供应商设置：<br>
<a href="http://static.domain.com" target="_blank" rel="noopener noreferrer">static.domain.com</a>(自定义域名）&nbsp;-&gt; CNAME -&gt;&nbsp;<a href="http://my-bucket.oss-cn-shenzhen.aliyuncs.com" target="_blank" rel="noopener noreferrer">my-bucket.oss-cn-shenzhen.aliyuncs.com</a></p>
<h3> HTTPS证书托管</h3>
<p>上传证书，开启 HTTPS<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348442889-df82e3da-e07d-451e-af99-45f40f7e789a.png#averageHue=%23e6e4e0&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=340&amp;id=u94fa9d74&amp;originHeight=340&amp;originWidth=1586&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=228171&amp;status=done&amp;style=none&amp;taskId=u51004980-ecd6-4be3-914a-75f0b84cec0&amp;title=&amp;width=1586" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348569375-f32bac2c-25ae-4b38-a2e8-3d3d5a42164c.png#averageHue=%23f4f3f3&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=904&amp;id=u6ae4a65b&amp;originHeight=904&amp;originWidth=1607&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=308821&amp;status=done&amp;style=none&amp;taskId=uaa66eaf6-767e-47db-92fa-fdeaeb76403&amp;title=&amp;width=1607" alt=""></p>
<p>如果没有证书，查看教程获取：<a href="https://github.com/levy9527/blog/issues/5" target="_blank" rel="noopener noreferrer">🔒免费开启HTTPS</a></p>
<h3> 公共读</h3>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348615962-e6695e07-9c22-4095-876a-9ea0cd412d0d.png#averageHue=%23fcfaf9&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=409&amp;id=uc420f547&amp;originHeight=409&amp;originWidth=1748&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=192573&amp;status=done&amp;style=none&amp;taskId=u78cd38c9-f1b7-4132-afc0-3bbf461b01d&amp;title=&amp;width=1748" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>这样可以解决访问链接超时的问题。</p>
<h3> CORS跨域设置</h3>
<p>在基础设置下，找到跨域设置<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1547111265557-dd3885fc-1007-4dfe-bef9-1e70a3578f0f.png#averageHue=%23fefdfd&amp;height=116&amp;id=hyGnS&amp;originHeight=314&amp;originWidth=2022&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;status=done&amp;style=none&amp;title=&amp;width=747" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1547111287199-072507c0-02d4-4cdb-8be7-0bccc13c096c.png#averageHue=%239fa69d&amp;height=159&amp;id=JDTLh&amp;originHeight=438&amp;originWidth=2058&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;status=done&amp;style=none&amp;title=&amp;width=747" alt=""></p>
<p>在来源中设置域名，或ip地址。下面给出最简单的示例为 *，实际可以根据需要填写允许的域名，一行一个。</p>
<ul>
<li>将allowed origins设置成&nbsp;<code>*</code></li>
<li>将allowed methods设置成<code>GET, POST, PUT,&nbsp;DELETE, HEAD</code></li>
<li>将allowed headers设置成&nbsp;<code>*</code></li>
<li>将expose headers设置成
<ul>
<li><code>etag</code></li>
<li><code>x-oss-request-id</code></li>
</ul>
</li>
</ul>
<p>这样可以解决字体无法显示、JavaScript跨域的问题。</p>
<h2> 华为云OBS</h2>
<h3> 跨域设置</h3>
<p>华为云的入口如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702347220980-e3545244-04d2-4435-bc87-f277a6ddbf91.png#averageHue=%23fefdfc&amp;clientId=u1ece0f42-2e8c-4&amp;from=paste&amp;height=570&amp;id=ub4d8b2f8&amp;originHeight=570&amp;originWidth=1099&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=47529&amp;status=done&amp;style=none&amp;taskId=uba2e51ff-d581-4e8c-8dcc-95be2bca9cf&amp;title=&amp;width=1099" alt=""><br>
具体规则的填写是类似阿里云OSS的。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702348727824-91972218-3050-4346-838e-f33b317dba14.png#averageHue=%23e7e7e7&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=710&amp;id=u532a6905&amp;originHeight=710&amp;originWidth=1221&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=43351&amp;status=done&amp;style=none&amp;taskId=u606e3ead-a17a-41ce-bd9c-801c4ea1259&amp;title=&amp;width=1221" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702347742834-834879a6-3610-4b57-b0d3-73c7bcdead4e.png#averageHue=%23eaeae6&amp;clientId=u60f42bc1-4481-4&amp;from=paste&amp;height=342&amp;id=u96fb23ed&amp;originHeight=342&amp;originWidth=1808&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=302360&amp;status=done&amp;style=none&amp;taskId=u8ae79e13-fed5-4ff3-9214-5f220758b8e&amp;title=&amp;width=1808" type="image/"/>
    </item>
    <item>
      <title>Docker 构建镜像、推送、启动实用脚本</title>
      <link>https://levy.vip/devops/docker-build-and-push-script.html</link>
      <guid>https://levy.vip/devops/docker-build-and-push-script.html</guid>
      <source url="https://levy.vip/rss.xml">Docker 构建镜像、推送、启动实用脚本</source>
      <description>Docker 构建镜像、推送、启动实用脚本 misc 存储多份 docker 认证信息： mkdir &amp;quot;~/.project1&amp;quot; mkdir &amp;quot;~/.project2&amp;quot; docker --config ~/.project1 login registry.example.com -u &amp;lt;username&amp;gt; -p &amp;lt;deploy_token&amp;gt; docker --config ~/.project2 login registry.example.com -u &amp;lt;username&amp;gt; -p &amp;lt;deploy_token&amp;gt;</description>
      <pubDate>Fri, 08 Dec 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Docker 构建镜像、推送、启动实用脚本</h1>
<h2> misc</h2>
<p>存储多份 docker 认证信息：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>使用：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h2> <a href="http://get-version.sh" target="_blank" rel="noopener noreferrer">get-version.sh</a></h2>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> <a href="http://build-image.sh" target="_blank" rel="noopener noreferrer">build-image.sh</a></h2>
<p>support command:</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><code>build-image.sh</code> (remember to replace <code>xxx</code> with true value)：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> <a href="http://startup.sh" target="_blank" rel="noopener noreferrer">startup.sh</a></h2>
<p><code>startup.sh</code></p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
    </item>
    <item>
      <title>缩减Python应用的镜像体积</title>
      <link>https://levy.vip/devops/reduce-python-image-size.html</link>
      <guid>https://levy.vip/devops/reduce-python-image-size.html</guid>
      <source url="https://levy.vip/rss.xml">缩减Python应用的镜像体积</source>
      <description>缩减Python应用的镜像体积 背景 当你为 LLM 应用构建镜像时，发现整个过程很慢，一看镜像体积：好家伙，1.76 G！ 能不能减少镜像体积，缩短打包时间啊？本文将分享两招实用的技巧，让 Python 应用的镜像体积减少 50%。</description>
      <pubDate>Sat, 09 Dec 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 缩减Python应用的镜像体积</h1>
<h2> 背景</h2>
<p>当你为 LLM 应用构建镜像时，发现整个过程很慢，一看镜像体积：好家伙，1.76 G！<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1704179561250-cca8b370-edb0-4d90-a0d1-4e4db5e25a71.png" alt=""><br>
能不能减少镜像体积，缩短打包时间啊？本文将分享两招实用的技巧，让 Python 应用的镜像体积减少 50%。</p>
<!-- more -->
<h2> 原始Dockerfile</h2>
<p>先来看看 Dockerfile 的原始模样：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 使用slim镜像</h2>
<p>最简单快捷的优化方式，是修改第一行代码，使用 slim 镜像。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>重点是：为什么用 slim，而不是 alpine？</p>
<p>这里我们就要搞清楚，Alpine 是 Linux 众多发行版中的一员，与 CentOS、Ubuntu、Archlinux 之类一样，只是一个发行版的名字，主打一个小巧安全。</p>
<p>然而，alpine 镜像有一个陷阱，它使用的标准库与大多数发行版不同，它使用的是 <code>musl libc</code>，与常用的标准库 <code>glibc</code>并不兼容。</p>
<p>而在 alpine 镜像中，使用 Wheel 文件(后缀为 .whl) 安装 Python 依赖时，可能会出现兼容性问题，因为 Wheel 会与 C 语言的扩展库有关联，而这些 C extensions 不一定与 <code>musl libc</code>兼容，尤其是使用到了 NumPy、 Pandas 的时候。</p>
<p>那么为什么 slim 镜像又可以呢？因为 slim 镜像是基于 Debian 的, 使用的是 <code>glibc</code>，通过删除了许多非必需的软件包方式，优化了体积。</p>
<p>因此，在 Python 中，最稳妥的做法是，做 slim 镜像，而不是 alpine。</p>
<h2> 减少层数、取消本地缓存</h2>
<p>来看这两行代码：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>安装依赖，应该一步到位，我们可以把这两行压缩成一行，以减少 docker layer 数量。</p>
<p>另外，pip 安装依赖时，会在本地生成缓存，而这对于镜像来说是无用的，可以添加参数 <code>--no-cache-dir</code>禁止此行为。</p>
<p>则上述两行代码优化如下：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h2> 优化效果</h2>
<p>第一次优化，使用 slim 镜像：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1704179592750-ab34f91e-7ab7-45e4-856a-314341602a74.png" alt=""></p>
<p>第二次优化，减少层数、取消本地缓存：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1704179609044-68068d48-0b3b-4f5f-be9a-7745a9173f98.png" alt=""><br>
小小的改动，大大的变化！</p>
<h2> 参考</h2>
<p><a href="https://icloudnative.io/posts/intro-guide-to-dockerfile-best-practices/" target="_blank" rel="noopener noreferrer">https://icloudnative.io/posts/intro-guide-to-dockerfile-best-practices/</a><br>
<a href="https://icloudnative.io/posts/docker-images-part2-details-specific-to-different-languages/#jdk-vs-jre" target="_blank" rel="noopener noreferrer">https://icloudnative.io/posts/docker-images-part2-details-specific-to-different-languages</a><br>
<a href="https://www.ardanlabs.com/blog/2020/04/docker-images-part3-going-farther-reduce-image-size.html" target="_blank" rel="noopener noreferrer">https://www.ardanlabs.com/blog/2020/04/docker-images-part3-going-farther-reduce-image-size</a></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1704179561250-cca8b370-edb0-4d90-a0d1-4e4db5e25a71.png" type="image/png"/>
    </item>
    <item>
      <title>sh与bash的区别</title>
      <link>https://levy.vip/devops/what-is-the-difference-between-sh-and-bash.html</link>
      <guid>https://levy.vip/devops/what-is-the-difference-between-sh-and-bash.html</guid>
      <source url="https://levy.vip/rss.xml">sh与bash的区别</source>
      <description>sh与bash的区别 结论：如果可移植性很重要，那么应该使用 sh！一般编写 Dockerfile　时，有关的脚本优先使用 sh。 常见问题：明明是存在的、可执行的shell脚本，却在容器报错 No such file or directory，很可能是因为shell脚本开头声明了bash，但容器里只能执行 sh。</description>
      <pubDate>Thu, 03 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> sh与bash的区别</h1>
<p>结论：如果可移植性很重要，那么应该使用 sh！一般编写 Dockerfile　时，有关的脚本优先使用 sh。</p>
<p>常见问题：明明是存在的、可执行的shell脚本，却在容器报错 <code>No such file or directory</code>，很可能是因为shell脚本开头声明了bash，但容器里只能执行 sh。</p>
<!-- more -->
<p>视频里有实战演示：</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1691066962763-bfbe3c1a-cb4f-43bc-b181-062eabee9529.png#averageHue=%23e4e5e7&amp;clientId=ue1ff8f12-10ba-4&amp;from=paste&amp;height=196&amp;id=u6521b3f3&amp;originHeight=392&amp;originWidth=1428&amp;originalType=binary&amp;ratio=2&amp;rotation=0&amp;showTitle=false&amp;size=235208&amp;status=done&amp;style=none&amp;taskId=uef2009af-bd00-4e0e-a215-9cca4c37ed2&amp;title=&amp;width=714" type="image/"/>
    </item>
    <item>
      <title>旧文章精选</title>
      <link>https://levy.vip/frontend/old-articles.html</link>
      <guid>https://levy.vip/frontend/old-articles.html</guid>
      <source url="https://levy.vip/rss.xml">旧文章精选</source>
      <description>旧文章精选 📦vue组件发布npm最佳实践 🔨揭秘vue-sfc-cli：组件研发利器 🚀Github集成TravisCI：自动发布 ⚡Github集成Netlify：快速预览PR 🌪自动化的Github Workflow 🤖如何写一个GithubApp 🔒免费开启HTTPS 🕸捕获与改写HTTPS请求</description>
      <pubDate>Tue, 03 Sep 2019 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 旧文章精选</h1>
<ul>
<li><a href="https://github.com/levy9527/blog/issues/2" target="_blank" rel="noopener noreferrer">📦vue组件发布npm最佳实践</a></li>
<li><a href="https://github.com/levy9527/blog/issues/7" target="_blank" rel="noopener noreferrer">🔨揭秘vue-sfc-cli：组件研发利器</a></li>
<li><a href="https://github.com/levy9527/blog/issues/1" target="_blank" rel="noopener noreferrer">🚀Github集成TravisCI：自动发布</a></li>
<li><a href="https://github.com/levy9527/blog/issues/4" target="_blank" rel="noopener noreferrer">⚡Github集成Netlify：快速预览PR</a></li>
<li><a href="https://github.com/levy9527/blog/issues/12" target="_blank" rel="noopener noreferrer">🌪自动化的Github Workflow</a></li>
<li><a href="https://github.com/levy9527/blog/issues/10" target="_blank" rel="noopener noreferrer">🤖如何写一个GithubApp</a></li>
<li><a href="https://github.com/levy9527/blog/issues/5" target="_blank" rel="noopener noreferrer">🔒免费开启HTTPS</a></li>
<li><a href="https://github.com/levy9527/blog/issues/11" target="_blank" rel="noopener noreferrer">🕸捕获与改写HTTPS请求</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>前端项目性能优化实战</title>
      <link>https://levy.vip/frontend/performance-optimization-in-action.html</link>
      <guid>https://levy.vip/frontend/performance-optimization-in-action.html</guid>
      <source url="https://levy.vip/rss.xml">前端项目性能优化实战</source>
      <description>前端项目性能优化实战 本文将分享常用的 Web 页面性能分析工具，以及一个前端项目性能优化的实战经验。</description>
      <pubDate>Wed, 27 Jan 2021 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 前端项目性能优化实战</h1>
<p>本文将分享常用的 Web 页面性能分析工具，以及一个前端项目性能优化的实战经验。</p>
<!-- more -->
<h2> 检测</h2>
<p>使用两个工具分析项目首页性能情况：</p>
<ol>
<li><a href="https://developers.google.com/speed/pagespeed/insights/" target="_blank" rel="noopener noreferrer">https://developers.google.com/speed/pagespeed/insights/</a></li>
<li><a href="https://tools.pingdom.com/" target="_blank" rel="noopener noreferrer">https://tools.pingdom.com/</a></li>
</ol>
<p>得到结果如下：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1611714344531-6501362c-526d-40e5-b1a4-a3399a544ea1.png" alt=""><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607738629800-10ff9829-ea0c-402b-b826-1d05903ee302.png" alt=""><br>
可以看到，首页超过50%的请求都与图片有空，优化空间比较大，因此第一步应该是优化图片加载。</p>
<h2> 图片优化</h2>
<p>关于图片，PageSpeed 的优化建议如下：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607739432571-c4ebd1bb-bf91-4ced-afca-ad70b84c4de6.png" alt=""><br>
根据文章《<a href="https://zhuanlan.zhihu.com/p/99769484" target="_blank" rel="noopener noreferrer">把图片优化指南做成一个组件：v-img</a>》，找到首页的图片相关的代码：</p>
<ol>
<li>把 <code>&lt;img&gt;</code>&nbsp; 元素修改成 <code>&lt;v-img/&gt;</code>&nbsp;，注意设置 width 或 height</li>
<li>把 <code>&lt;div ``style="background-image: url(img-url)"``&gt;&lt;/div&gt;</code> 修改成 <code>&lt;div v-img="{src: img-url}"&gt;&lt;/div&gt;</code></li>
</ol>
<p>注意：img-url 应该是 oss 的链接，并且是 https 协议。<br>
如果是 http 协议，或不指定协议 //img-url，则很可能会出现下图的情况：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1611714403654-591acc14-7c8e-4882-a781-a5271bb0b428.png" alt=""></p>
<p>如果图片是放在项目中，且项目并没有部署到 oss，则无法享受自动加载 webp 格式图片的福利。</p>
<p>则在此环节，一次性做到了上图中的三个优化点。</p>
<h2> 提高TTFB时间</h2>
<p>来看下一条优化建议：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607914614811-2ea4a4f6-0f0c-4222-bc6f-112f3512bfbb.png" alt=""><br>
因为项目是服务端渲染的，有些请求是在服务端做了。找到相关代码：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607914815788-19181de1-6526-46f1-8ed2-e4c3bd130e7a.png" alt=""><br>
经过分析，上述代码存在两个问题：</p>
<ol>
<li>可在客户端执行却放在了服务端</li>
<li>可并行执行却写成了串行</li>
</ol>
<p>修改如下：</p>
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607914801350-ca4cadfe-9750-47be-98f4-fd5a305bb408.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<hr>
<p>通过分析请求日志发现，有一个请求应该是在客户端发送，代码本意也是在客户端执行，却在服务端也执行了。<br>
找到发送请求的代码：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607929114683-23cb4794-bc67-4bed-8e95-0a8f493cb1e6.png" alt=""><br>
原来是代码写在了 created 里，这是个经典的案例：为了让请求更早一点发送，不写在 mounted 钩子，而写在 created 里，导致请求分别在 server-side 与 client-side 都执行了。具体说明请看 <a href="https://ssr.vuejs.org/guide/universal.html#component-lifecycle-hooks" target="_blank" rel="noopener noreferrer">vue ssr 官方文档</a>。</p>
<p>修改如下：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607929358177-06449ad6-8251-444d-9ae3-1e2ae93b8fef.png" alt=""></p>
<h2> 移除未使用的 Javascript</h2>
<p>来看下一条优化建议：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607931113342-9fbf0d58-58dd-4d77-9628-cb2a1182d957.png" alt=""><br>
因为经过多次迭代，有可能某些功能曾经上线过，后来被下线，但当时代码没删干净，所以留下一些现在没用的第三方库。根据建议，找到这些引入的 js，确保不影响正常功能后，删除即可。</p>
<p>此时注意用到以下基本操作：</p>
<ol>
<li>google/github 查询库的用途</li>
<li>git history/annotate 查看是何人何时引入、并在何处使用的，如何代码不能表明意图，则最好找到相关人员询问是有何意图。</li>
</ol>
<h2> 延迟静态资源的加载</h2>
<p>来看另一个相关的建议：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1607932915571-42f62757-85e0-4893-8a1f-5b3d103da94c.png" alt=""><br>
如果有些第三方 js 确实有用到，但却不是关键资源，则可以延迟其加载或解析时机，缩短阻塞时间。</p>
<p>以一些第三方代码为例，它们并不是关键资源，可以在 window.onload 事件触发后，再加载</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 启用文本压缩</h2>
<p>首先是启用文本压缩，采用 gzip 是最简单的方式。对应的请求头是：<strong>Content-Encoding</strong><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1612505764342-9505cfe1-9025-49b0-b1fc-3e49e6d9f2c5.png" alt=""></p>
<p>因为静态资源是放到对象储存服务上的，故应该修改相关的配置。</p>
<ul>
<li>如果是阿里云OSS，<a href="https://help.aliyun.com/document_detail/31913.html?spm=5176.11065259.1996646101.searchclickresult.76556e6eoNS81O" target="_blank" rel="noopener noreferrer">点击查看参考文档</a></li>
<li>如果是华为云OBS，<a href="https://support.huaweicloud.com/usermanual-cdn/cdn_01_0119.html" target="_blank" rel="noopener noreferrer">点击查看参考文档</a></li>
</ul>
<p>这里要注意的是，对于静态资源的访问，最好使用自定义域名，而不是存储桶的域名，方便CDN做加速优化。</p>
<h2> 优化缓存策略</h2>
<p>下一条相关的优化建议是缓存策略，对应的请求头是：<strong>Cache-Control</strong><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1612506152108-a13a4684-6b3c-46d2-8fa3-0b20fda0d4ec.png" alt=""><br>
简单来说，就是在静态资源（html 除外）的 HTTP&nbsp;响应头中设置如下字段：</p>
<div class="language-http line-numbers-mode" data-ext="http"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>以阿里云&nbsp;oss&nbsp;为例进行说明，其他静态资源存储如 obs、S3 都是同理。</p>
<p>对于少量的资源，可以进行手工操作。打开&nbsp;<a href="https://github.com/aliyun/oss-browser" target="_blank" rel="noopener noreferrer">oss-browser</a>，找到相应资源：</p>
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1574909023658-459fff93-ad99-43ad-92c1-bf94d5cff9c6.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1574909060558-751aeab8-c08c-4a84-95f4-f3fbe5e13e24.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>本中使用的&nbsp;oss-browser&nbsp;版本，一次只能对一个资源进行&nbsp;HTTP&nbsp;头的设置，操作十分不便，可以<a href="https://help.aliyun.com/document_detail/31913.html?spm=a2c4g.11186623.4.1.7e863bdb6IwtQq" target="_blank" rel="noopener noreferrer">登录阿里云控制台进行批量操作</a>。</p>
<p>当然，最根本的解决办法，是使用 <a href="https://help.aliyun.com/document_detail/120057.html?spm=a2c4g.11186623.6.711.c31a2d24TVqy6d" target="_blank" rel="noopener noreferrer">阿里云oss命令行工具</a> 上传的时候就进行设置。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>注意，在命令行里别乱设置 Content-Encoding:gzip，否则会出现下面的情况，页面都打不开，具体说明<a href="https://appuals.com/how-to-fix-err_content_decoding_failed-error/" target="_blank" rel="noopener noreferrer">查看详情</a><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1574914337532-52c323e6-b22c-45a4-b514-c2614762909a.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1611714344531-6501362c-526d-40e5-b1a4-a3399a544ea1.png" type="image/png"/>
    </item>
    <item>
      <title>Git最佳实践</title>
      <link>https://levy.vip/git/git-best-pratices.html</link>
      <guid>https://levy.vip/git/git-best-pratices.html</guid>
      <source url="https://levy.vip/rss.xml">Git最佳实践</source>
      <description>2020-09-21 Git最佳实践 精简提交 一次只提交一个“瘦”的功能，同时只包含相关改动文件。例如，对于两个错误的修复应该进行两次不同的提交。 如果发现写提交信息时，需要写两点以上; &amp;nbsp;则可以考虑拆分提交。 频繁提交 一次提交应只对应一个“瘦”的功能。从而达到频繁提交的目标。 经常性地提交改动可以确保不会出现特别庞大的提交，同时也可以比较精准地对应到所需要的改动上。 此外，通过频繁地提交也可以比较快速地和其他开发人员来共享你的改动。同样也会避免在整合代码时出现过多的合并冲突。相反的，非常庞大的提交会加大整合代码时出现冲突的风险，解决这些冲突也会非常复杂。</description>
      <pubDate>Mon, 21 Sep 2020 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>2020-09-21</p>
<h1> Git最佳实践</h1>
<h2> 精简提交</h2>
<p>一次只提交一个“瘦”的功能，同时只包含相关改动文件。例如，对于两个错误的修复应该进行两次不同的提交。<br>
如果发现写提交信息时，需要写两点以上; &nbsp;则可以考虑拆分提交。</p>
<h2> 频繁提交</h2>
<p>一次提交应只对应一个“瘦”的功能。从而达到频繁提交的目标。<br>
经常性地提交改动可以确保不会出现特别庞大的提交，同时也可以比较精准地对应到所需要的改动上。</p>
<p>此外，通过频繁地提交也可以比较快速地和其他开发人员来共享你的改动。同样也会避免在整合代码时出现过多的合并冲突。相反的，非常庞大的提交会加大整合代码时出现冲突的风险，解决这些冲突也会非常复杂。</p>
<h2> 不要提交不完整的改动</h2>
<p>虽然原则上来说不要提交一些还没有完成的改动，但是对于一个非常庞大的新功能来说，也并不意味着你必须整体完成这个功能后才可以提交。恰恰相反，你必须把那些改动正确地分割成一些有意义的逻辑模块来进行频繁地提交。</p>
<p>如果你仅仅是因为急着想要下班，或者是想要得到一个干净的工作副本（比如想要切换到另一个分支上），你可以利用 Git 所提供的储藏（Stash）功能来解决这些问题。切记不要把那些不完整的改动提交到仓库中。</p>
<h2> 提交前测试那些改动</h2>
<p>不要理所当然地认为自己完成的改动都是正确的。所有的改动一定要通过彻底地测试才表示它真正地被完成了。</p>
<h2> 版本控制不是备份系统</h2>
<p>版本控制系统具有一个很强大的附带功能，那就是服务器端的备份功能。但是千万不要把 VCS 仅仅当成一个备份系统。特别需要注意的是，只能提交那些有意义的改动。</p>
<h2> Github实例</h2>
<h3> 一个功能对应一个分支</h3>
<p>下面是好的示例： 格式化代码，也应该单独一个PR<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344980430.png" alt=""><br>
下面是不好的示例：因为一个PR修改了不同的主题内容<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344985874.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344990582.png" alt=""></p>
<h3> 提交“瘦”的PR</h3>
<p>参考文章：<a href="https://deliveroo.engineering/2017/09/06/play-pull-request-roulette.html#ideas-to-make-your-prs-more-review-friendly" target="_blank" rel="noopener noreferrer">https://deliveroo.engineering/2017/09/06/play-pull-request-roulette.html#ideas-to-make-your-prs-more-review-friendly</a><br>
其中最重要的一点：不要一次提交一个很大改动的PR，否则别人很难 review，要学会拆分步骤。<br>
下面是一个 PR 示例：<br>
拆分前，包含了35个改动，很难 review<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344998899.png" alt=""></p>
<p>下图是拆分后：<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345004360.png" alt=""></p>
<p>单个PR的改动文件只有11个<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345009991.png" alt="">每个 PR&nbsp;改动的文件少了，这样&nbsp;review&nbsp;起来就更容易了。</p>
<h3> 使用正确的标题</h3>
<p><a href="https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits" target="_blank" rel="noopener noreferrer">相关规范看这里</a></p>
<p>另外，请回答：出于什么原因需要进行这次修改？具体改动了些什么？</p>
<ul>
<li>使用一定要使用现在时祈使句（例如要使用 change ，而不是 changed 或 changes）。</li>
<li>优先使用正面肯定语句，而不是否定句。</li>
</ul>
<p>好的示例：<code>docs: extraQuery 的正确使用方法</code><br>
不好的示例：<code>docs: 更新不直观的例子</code></p>
<h3> 根据模板填写PR描述</h3>
<p>这是我们 Github 的 PR 模板，融合了我们的最佳实践<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345014045.png" alt=""><br>
下面是实际的好的例子<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345019293.png" alt=""></p>
<h3> 自动关闭issue</h3>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345024941.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当pr合并时，将自动close issue</p>
<h3> 1+2 review 规则</h3>
<p>1 是指发起 PR 的人，2 是指进行 code review 的人。也即，每一个 PR，至少要经过两个团队成员 approve 才能合并。</p>
<blockquote>
<p>上面是针对 github 的协作，项目组中可酌情变为 1+1 规则</p>
</blockquote>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345031945.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345036964.png" alt=""></p>
<h3> 礼貌提问</h3>
<p>在 github 向人提问时，需要有礼貌。当提出 feature request时，还要说明自己的情况，尽可能提供更多的信息给对方。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682345041577.png" alt=""><br>
上面的示例有三个重点：</p>
<ol>
<li>开头表达感谢</li>
<li>中间说明己方的使用情况，并给出相应链接</li>
<li>最后参考业界已有实现，给出一个方案设想，并给出相应链接</li>
</ol>
<h2> 学习资源</h2>
<ul>
<li><a href="https://git.oschina.net/progit/" target="_blank" rel="noopener noreferrer">Pro Git</a></li>
<li><a href="https://learngitbranching.js.org/?NODEMO" target="_blank" rel="noopener noreferrer">https://learngitbranching.js.org</a></li>
</ul>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344980430.png" type="image/png"/>
    </item>
    <item>
      <title>Git代码合并指南</title>
      <link>https://levy.vip/git/git-definitive-guide-to-merge-code.html</link>
      <guid>https://levy.vip/git/git-definitive-guide-to-merge-code.html</guid>
      <source url="https://levy.vip/rss.xml">Git代码合并指南</source>
      <description>Git代码合并指南 前言 合并时代码常见问题是冲突、提交错代码以及合并错分支，本文将说明这些问题的解决方案，为代码合并打下坚实的基础，以应对未来可能出现的分支模型多样化、协作流程复杂化的场景。 在说明问题前，先定义一些概念： feat：指代功能分支 dev 与 test：指代两条不同的长驻分支，它们具有以下特点： 受保护，不能直接推送 不会被删除 二者之间不直接合并，也即合并方式一般是 feat -&amp;gt; dev，feat -&amp;gt; test MR：merge request。代码合并请求</description>
      <pubDate>Thu, 28 Apr 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Git代码合并指南</h1>
<h2> 前言</h2>
<p>合并时代码常见问题是冲突、提交错代码以及合并错分支，本文将说明这些问题的解决方案，为代码合并打下坚实的基础，以应对未来可能出现的分支模型多样化、协作流程复杂化的场景。<br>
在说明问题前，先定义一些概念：</p>
<ul>
<li>feat：指代功能分支</li>
<li>dev 与 test：指代两条不同的长驻分支，它们具有以下特点：
<ul>
<li>受保护，不能直接推送</li>
<li>不会被删除</li>
<li>二者之间不直接合并，也即合并方式一般是 feat -&gt; dev，feat -&gt; test</li>
</ul>
</li>
<li>MR：merge request。代码合并请求</li>
</ul>
<p>以及说明本文解决冲突涉及到的工具及平台：</p>
<ul>
<li>使用 IDEA 解决冲突（JetBrains系列的工具都适用）</li>
<li>使用 GitLab 托管代码</li>
</ul>
<h2> 功能分支合并长驻分支冲突</h2>
<p>这是最常见的场景：feat1 与 feat2 并行开发，当提交MR（ feat1 -&gt; dev ）时，发现冲突了，无法合并。<br>
下面先给出解决思路，再给出图文操作步骤。</p>
<h3> 解决思路</h3>
<ol>
<li>因为合并的方向是 feat -&gt; dev，所以解决冲突应该是在本地 dev 合并 feat</li>
<li>又因为本地 dev 不能向远程推送，因而需要基于 dev 切一个新分支 conflict/resolved</li>
<li>推送 conflict/resolved 分支</li>
<li>提交 MR：conflict/resolved -&gt; dev</li>
</ol>
<h3> 操作步骤</h3>
<ol>
<li>本地切换到 dev 分支，更新代码</li>
<li>合并相应的 feat 分支</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344287512.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>弹出冲突提示，点击合并</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344293749.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>首先处理无冲突的代码，点击下图红框处</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344298158.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>再根据情况，选择合并代码或丢弃代码。</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344303228.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>在 dev 分支上切出新分支，推荐命名为 conflict/xxx</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344307534.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>推送代码，提交MR（conflict -&gt; dev)，记得勾选合并后删除</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344312127.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 功能分支被污染</h2>
<p>分支一多，人难免失误，很可能造成 feat 分支被污染，即当提MR（feat -&gt; test）时，出现不想合并到 test的代码或提交记录。<br>
这种场景的出现可能有多种原因：</p>
<ol>
<li>研发过程中出现误操作，如出现了 dev -&gt; feat 的合并</li>
<li>feat 分支的基线分支搞错了，如从 dev 切出了 feat</li>
</ol>
<h3> 解决思路</h3>
<ol>
<li>基于目标分支如 (test 分支）切一个干净的分支 clean</li>
<li>使用 cherry-pick，挑选自己想要的提交</li>
<li>再提交MR（clean -&gt; test）</li>
</ol>
<p>注意的是，要按提交顺序进行 cherry-pick，以避免遗漏或出错。</p>
<h3> 操作步骤</h3>
<ol>
<li>更新目标分支（在这里是 test）</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344317048.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>基于 test 切新分支，这里示例命名为：clean</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344322358.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<ol>
<li>在拥有最新代码的分支（这里是 feat） 找到并选中相应的提交记录</li>
<li>右键，点击 Cherry-Pick<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344326902.png" alt=""></li>
<li>则相应的提交记录就会合并到 clean 分支</li>
<li>推送 clean，提交MR（clean -&gt; test）</li>
</ol>
<h2> 挑选别的分支部分代码合并</h2>
<p>有可能会出现这样一种场景：</p>
<ul>
<li>最新的生产代码里，假设版本为v1.3.0，包含了 feat 分支的代码</li>
<li>为了减少分支的冗余，代码一旦上生产后，就会清除相应的功能分支，也即此时仓库里没有 feat 分支了</li>
<li>客户方部署的版本代码为 v1.1.0，而客户不想升级到最新的版本，只想要 feat 分支相应的功能</li>
</ul>
<p>此时该如何是好？</p>
<h3> 解决思路</h3>
<p>其实只要触发“挑选”关键字，就可以考虑使用 cherry-pick。<br>
feat 分支就算被删了，只要提交记录还在，那也没关系：</p>
<ul>
<li>在v1.3.0 的代码库中，按分支筛选，找出 feat 分支对应的提交记录</li>
<li>通过 cherry-pick 把 feat 分支的代码合并到客户方的代码分支即可</li>
</ul>
<p>注意：毕竟跨越了版本，无法保证合并过去后的代码一定能正确工作，需要进行充分地测试。</p>
<h3> 操作步骤</h3>
<p>此操作本质还是 cherry-pick，参考前面 cherry-pick 的示例即可。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682344287512.png" type="image/png"/>
    </item>
    <item>
      <title>Git查看历史记录小技巧</title>
      <link>https://levy.vip/git/git-history-two-tricks-in-idea.html</link>
      <guid>https://levy.vip/git/git-history-two-tricks-in-idea.html</guid>
      <source url="https://levy.vip/rss.xml">Git查看历史记录小技巧</source>
      <description>Git查看历史记录小技巧 分享两个Git的小技巧， 都是关于在 IDEA 里查看Git的历史记录的。 这两个技巧，简单却实用。面对年代久远、团队人员流失严重的代码，靠的就是这两个技巧, 从提交记录里品读岁月史书，从蛛丝马迹中寻找遗失的真相。</description>
      <pubDate>Fri, 11 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Git查看历史记录小技巧</h1>
<p>分享两个Git的小技巧， 都是关于在 IDEA 里查看Git的历史记录的。</p>
<p>这两个技巧，简单却实用。面对年代久远、团队人员流失严重的代码，靠的就是这两个技巧, 从提交记录里品读岁月史书，从蛛丝马迹中寻找遗失的真相。</p>
<!-- more -->
<p>第一个是叫 <code>annotate with git blame</code>。<br>
在IDEA的行号这个位置，右键，再点击即可。如图所示：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img.PNG" alt=""></p>
<p>效果就是，每一行代码都会显示，该行代码是由谁提交的、 什么时候提交的。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img_1.PNG" alt=""></p>
<p>在合并冲突的时候也可以用这个技巧。</p>
<p>对左右两边进行<code>git blame</code>一下， 然后就可以看到如图所示的情况：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img_2.PNG" alt=""></p>
<p>这样就能提供更多的信息帮助解决冲突。</p>
<p>就算冲突无法自己解决，也至少能知道提交代码的是谁，可以找到作者去进行沟通。</p>
<p>当然<code>git blame</code> 是有一些注意点的。因为它本质上显示的是某一行代码的最后提交人， 也就是last modified的一个概念，而有些时候这并不意味着最后的修改人就是代码的原作者。</p>
<p>之所以这样，可能会有以下的原因:</p>
<ol>
<li>代码格式化</li>
<li>移动代码，比如说拷贝代码、迁移代码</li>
<li>合并代码，解决冲突</li>
</ol>
<p>上述操作都会改变最后修改人的这个属性，但此时显然最后修改人并非原作者。</p>
<p>这说明，有时候仅知道了某一行代码是由谁最近修改的还不够，还需要知道某一个文件经过了怎么样的修改。</p>
<p>这就引出了第二个小技巧了: <code>git show history</code>。</p>
<p>点击IDEA某个文件的空白处，然后右键，选择git，然后点击 show history。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img_3.PNG" alt=""></p>
<p>就会出现如图所示的这样的一个 git log的 界面。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img_4.PNG" alt=""></p>
<p>那么这样就可以看到这个文件从最初到至今经历过了怎样的修改、 有过哪些人在上面修改， 从而更好的进行记录追踪。</p>
<p>这两个技巧， 其实是越有经验就对你帮助越大的。因为你，年限越长，你看别人的代码的机会就越； 而如果你年限尚浅的话，更多的是你的代码被别人 review。</p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/2023-08-12/img.PNG" type="image/"/>
    </item>
    <item>
      <title>Git常用命令</title>
      <link>https://levy.vip/git/git-useful-commands.html</link>
      <guid>https://levy.vip/git/git-useful-commands.html</guid>
      <source url="https://levy.vip/rss.xml">Git常用命令</source>
      <description>Git常用命令 前言 本文将列举Git常见场景，并给出相应解决方案。 约定： 下文代码块中${}里面表示的是变量，具体值视情况而定，其余的都是正确可执行的命令。 推荐： 图形化交互式Git教程 配置 Mac/Linux 用户 执行以下操作 vi ~/.gitconfig</description>
      <pubDate>Mon, 21 Sep 2020 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Git常用命令</h1>
<h2> 前言</h2>
<p>本文将列举Git常见场景，并给出相应解决方案。</p>
<p>约定： 下文代码块中<code>${}</code>里面表示的是变量，具体值视情况而定，其余的都是正确可执行的命令。</p>
<p>推荐： <a href="https://learngitbranching.js.org/?locale=zh_CN" target="_blank" rel="noopener noreferrer">图形化交互式Git教程</a></p>
<h2> 配置</h2>
<p>Mac/Linux 用户 执行以下操作</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>Windows用户在桌面用户文件夹下有个.gitconfig隐藏文件，直接修改即可<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682343376774.png" alt=""><br>
补充以下内容</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 增强</h2>
<p>Mac或Linux用户，推荐安装<a href="https://github.com/robbyrussell/oh-my-zsh" target="_blank" rel="noopener noreferrer">https://github.com/robbyrussell/oh-my-zsh</a>，增强命令行体验。</p>
<h2> 记住账号密码</h2>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h2> 初始化</h2>
<p>在项目根目录，执行以下命令</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>或克隆远程仓库到本地，自动初始化</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 本地提交</h2>
<h3> 取消未暂存的修改</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 取消add</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 取消提交</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>或者</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 修正提交</h3>
<p>适用于提交信息有误或有遗漏，需要修正最新提交信息的场景。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> stash修改</h3>
<p>适用于当前功能开发并不完整，不能产生一次提交，但却要开发另外功能的场景</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 恢复stash</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 分支管理</h2>
<h3> 创建分支</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 查看远程分支</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 创建干净历史分支</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 删除分支</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 远程仓库</h2>
<h3> 远程仓库管理</h3>
<p>一般而言，称默认远程仓库为 origin，如果是通过本地 <code>git init</code> 初始化的，需要手动添加远程仓库</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果有多个远程仓库，为之取不同的名字即可。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>之后用以下命令进行推送：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>使用以下命令查看所有远程仓库</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 浅克隆</h3>
<p>适用于仓库很大，对过往历史不关心，想快速克隆的场景。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 克隆指定分支</h3>
<p>适用于只想要某一分支代码的场景。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 克隆失败因为文件名太长</h3>
<p>报错信息为：<code>error: unable to create file xxx.java: Filename too long</code></p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>注意，如果是使用 IDEA 进行克隆，很可能会看忽略该报错，但可以根据以下经验加以验证：如果 git clone 完成后，工作区并不干净（可以用 git status 检查）、不能切换分支，说明很可能就是上述情况。</p>
<h3> 强行推送</h3>
<p>适用于本地开发了一段时间，最近才在代码托管平台上初始化远程仓库的场景</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 取消错误的推送</h3>
<p>适用于推送了错误的提交后, 想取消该推送的场景</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 标签管理</h2>
<h3> 新建本地标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 删除本地标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 查看本地所有标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 推送本地标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 获取远程标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 删除远程标签</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 其他</h2>
<h3> cherry-pick</h3>
<ol>
<li>checkout目标分支(target branch)</li>
<li>选中相应的提交记录，右键</li>
<li>点击Cherry-pick<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682343384184.png" alt="image.png"></li>
<li>则相应的提交记录就会合并到目标分支</li>
</ol>
<h3> merge unrelated histories</h3>
<p>遇到上述问题时，可以使用 <code>--allow-unrelated-histories</code>&nbsp;，如</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> git log 丢失最新提交</h3>
<p>有 commit 从旧到新示意如下： c1 &lt;- c2 &lt;- c3(HEAD)</p>
<p>可能因为误操作，git reset 或 git checkout 到了 c2，此时再使用 git log，发现看不见 c3，怎么办？</p>
<p>可以使用</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>会显示出 HEAD 指针的变动轨迹，最新的变动在前面：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>找到 c3 对应的 hash，checkout 即可：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 查看分支创建时间</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>注意，本地分支可以查看到 clone 或 create 的日期：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>但远程分支，并不能确切地知道分支创建的日期：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 根据文件搜索历史</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 从所有提交中删除一个文件</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果代码已经推送到了远程仓库，还需要强制推送</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div>]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682343376774.png" type="image/png"/>
    </item>
    <item>
      <title>GitLab CI</title>
      <link>https://levy.vip/git/gitlab-ci.html</link>
      <guid>https://levy.vip/git/gitlab-ci.html</guid>
      <source url="https://levy.vip/rss.xml">GitLab CI</source>
      <description>GitLab CI 前言 GitLab 在企业内部还是比较通用的，其 CI 用起来个人也觉得比 Jenkins 顺手，因此在这里分享一下相关的实践经验。</description>
      <pubDate>Tue, 10 Jan 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> GitLab CI</h1>
<h2> 前言</h2>
<p>GitLab 在企业内部还是比较通用的，其 CI 用起来个人也觉得比 Jenkins 顺手，因此在这里分享一下相关的实践经验。</p>
<!-- more -->
<h2> 安装与配置</h2>
<h3> GitLab Runner 安装</h3>
<p>进行 Gitlab CI 的第一步是要安装 GitLab Runner。如果公司、团队内部已安装过，可以跳过这一步。</p>
<p>这里推荐使用 docker 的方式安装，复制以下命令执行即可：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>其他安装方式可查阅<a href="https://docs.gitlab.com/runner/install/docker.html#install-the-docker-image-and-start-the-container" target="_blank" rel="noopener noreferrer">文档</a>。</p>
<h3> GitLab Runner 注册</h3>
<p>GitLab Runner 安装以后，还要注册到 GitLab 的项目中才能使用，此步骤需要项目的 Maintainer 权限。</p>
<p>在注册前，可以先检查下，自己的项目中是否已有可以使用的 GitLab Runner（如果看不到 Settings，说明没有权限），如果有就记住其名字，然后跳过此步骤。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277808112.png" alt=""></p>
<p>GitLab Runner 根据范围分为<a href="https://docs.gitlab.com/ee/ci/runners/runners_scope.html" target="_blank" rel="noopener noreferrer">三种</a>。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682387098778.png" alt="image.png"></p>
<p>下面以 Specific Runner 为例进行说明。</p>
<p>进入项目如下界面：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682387102770.png" alt="image.png"></p>
<p>拿到 URL 及 token：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682387107092.png" alt="image.png"></p>
<p>执行命令进行注册:</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>根据提示输入内容，其中 URL 及 token 就是前面步骤中 Web 界面获取的信息。</p>
<p>命令行操作示例如下，注意两点即可：</p>
<ol>
<li>最重要的就是 URL 与 token，需要根据实际情况填写</li>
<li>其他参数可以与示例完全一致</li>
</ol>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>注册成功后，显示示例如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682387118062.png" alt="image.png"></p>
<h3> 提交.gitlab-ci.yml</h3>
<p>要想 Gitlab Runner 工作，还需要在项目根目录提交 .gitlab-ci.yml 文件。</p>
<p>建议提交.gitlab-ci.yml文件前，在 GitLab 先进行语法校验。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388474604.png" alt="image.png"></p>
<p>如果错误，会有提示。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388478169.png" alt="image.png"></p>
<p>如果配置成功，会看到 GitLab 的图标：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695784435960-bdac2473-54cd-4c42-85f3-cfbf18e21274.png" alt=""></p>
<p>如果图标如下所示，说明文件有误，比如文件名开头多了个空格🤦‍♂️：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695784470379-51a5693b-ecfc-4b7f-9b67-5b0b00ccf8d0.png" alt=""></p>
<p>以上就是 GitLab CI 所需的基本环境配置，接下来进行实战内容讲解。</p>
<h2> 合并代码前进行检查</h2>
<h3> 背景</h3>
<p>有的产品线使用 Jenkins 进行 CI，但又没设置好相应的 GitLab 插件，于是会形成这样一个流程：</p>
<ul>
<li>feature 分支发起 Merge Request</li>
<li>合并至受保护的分支</li>
<li>登录 Jenkins，点击构建</li>
<li>构建失败，原因：编译报错</li>
</ul>
<p>最后一点，非常难以忍受，因为代码已经合并进去了，木已成舟。此时面对编译报错，第一反应是解决报错，重新编译。但有没有一种可能，我根本不想要这些编译报错的代码呢？</p>
<p>笔者还是更倾向于防患于未然的思维模式，也即不能通过编译的代码，不允许合并至受保护的分支。而使用 Gitlab CI 来做这件事比 Jenkins 体验更丝滑，下面就来介绍一下具体的做法。</p>
<h3> 设置MR检查</h3>
<p>进入项目如下界面：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682387122013.png" alt="image.png"></p>
<p>勾选流水线必须成功。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388470210.png" alt="image.png"></p>
<h3> .gitlab-ci.yml 示例</h3>
<p>简单示例如下，根据实际情况修改：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>上述示例要设置成功，还要确保 .m2/settings.xml 文件存在。</p>
<h3> 效果</h3>
<p>当流水线还未结束时，不能提前合并代码，只能等待流水线成功。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388481821.png" alt="image.png"></p>
<p>如果流水线失败了，不能合并。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388485568.png" alt="image.png"></p>
<h2> 集成单元测试</h2>
<p>核心思路就是在 CI 环境运行 <code>mvn test</code>。</p>
<p>可能遇到的问题在于，由于项目依赖关系：</p>
<ol>
<li>旧代码中运行不通过的测试影响到了 <code>mvn test</code> 的结果</li>
<li>pom.xml 无法读取相关的配置</li>
</ol>
<p>首先，假设根目录为 parent，其下有三个子模块：</p>
<ul>
<li>a</li>
<li>b</li>
<li>common</li>
</ul>
<p>每个目录都有 pom.xml，其中所有子模块的属性值都来自于 parent 目录的 pom.xml。</p>
<p>而我们需要进行持续集成的模块是 b，则 maven 命令应该如下：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>则此时就跳过了子模块 a。</p>
<p>但如果子模块 b 又依赖了 common，此时 common 的遗留的测试用例报错了，那我们的解决办法只能是：一个个地解决报错。</p>
<p>当上述 maven 命令可以运行后，就可以修改 Gitlab CI 的配置，然后设置调度任务，让 Gitlab 每天都跑测试用例。一旦用例执行不通过，就会发邮件通知到我们。</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683436020492.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 线上发布 jar</h2>
<p>可以在前文的基础上，设置流水线自动发布 jar。</p>
<h3> Maven配置</h3>
<p>考虑到一个项目A，可能划分了多个模块，并非每个模块都需要发布 jar，可以修改对应模块的 pom.xml</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>则在项目根目录执行 deploy 命令即可：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> .gitlab-ci.yml 配置</h3>
<p>相应的配置如下：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>代码合并或有新的 commit 时，会执行流水线：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/git/1682388489368.png" alt="image.png"></p>
<h3> 拉取最新的jar</h3>
<p>在B项目中，如果要引用A项目打出来的 jar，记得拉取最新的版本，pom.xml 设置如下：</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 保存中间产物</h2>
<p>需要保存中间产物的一个场景是，流水线分多个阶段，后一个阶段依赖前一个阶段的产物。</p>
<p>举个例子：某个 java 项目，需要先编译输出 jar，再基于 jar 构建镜像。</p>
<p>相关示例代码如下：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 其他问题与解决方案</h2>
<h3> 环境变量</h3>
<p>Settings -&gt; CI/CD -&gt; Variables<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1701856198558-fbb7db94-a26a-46c2-9e05-06daa09f1ee7.png" alt=""></p>
<p>如果想隐藏变量值，不在日志中打印，可以在添加变量时勾选 Mask variable:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1701856682934-425f5ecc-a09f-493f-8a65-d6f2f74fb26a.png" alt=""></p>
<h3> Node.js</h3>
<p>本文主要以 Java 项目为例进行 Gitlab CI 相关的讲解，如果需要 Node.js 项目的示例，可以查看另外两篇文章：</p>
<ul>
<li><a href="/software-testing/use-playwright-for-ui-testing.html#%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90" target="blank">Playwright UI自动化测试</a></li>
<li><a href="/software-testing/use-postman-for-api-testing.html#%E5%BB%BA%E7%AB%8Bci%E4%BB%BB%E5%8A%A1" target="blank">Postman API自动化测试</a></li>
</ul>
<h3> 创建不了容器</h3>
<blockquote>
<p>ERROR: Preparation failed: adding cache volume: set volume permissions: running permission container "d1574748b77fc73a4319a45341af1f0eab983900d81885a02c017ff6c5559f28" for volume "runner-bzsttzs-project-2271-concurrent-0-cache-3c3f060a0374fc8bc39395164f415a70": starting permission container: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:319: getting the final child's pid from pipe caused "EOF"": unknown (linux_set.go:105:0s)</p>
</blockquote>
<p>可以尝试的方案：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果上述方法不行，可尝试重启 docker</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 本地成功，流水线失败</h3>
<p>如果流水线编译报错，本地编译通过，不用怀疑，一定是本地的问题。</p>
<p>本地之所以能编译通过，是因为有缓存。如果 pom.xml 没有设置 <code>&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</code>，编译时很可能使用的是缓存。</p>
<p>清除缓存拉取最新的包即可。</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 参考文档</h2>
<ul>
<li><a href="https://docs.gitlab.com/ee/ci/examples/" target="_blank" rel="noopener noreferrer">Gitlab CI 示例</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/variables/predefined_variables.html" target="_blank" rel="noopener noreferrer">预设的环境变量</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/jobs/job_control.html#specify-when-jobs-run-with-rules" target="_blank" rel="noopener noreferrer">rules规则说明</a></li>
</ul>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277808112.png" type="image/png"/>
    </item>
    <item>
      <title>再论Git Flow</title>
      <link>https://levy.vip/git/rethinking-git-flow.html</link>
      <guid>https://levy.vip/git/rethinking-git-flow.html</guid>
      <source url="https://levy.vip/rss.xml">再论Git Flow</source>
      <description>再论Git Flow 背景 团队目前使用的 Git 协作模式是： 对每个功能建立相应的 feat 分支 上研发、测试、UAT环境时，分别把相应的 feat 分支合并进入长驻 dev/test/uat 如有冲突，则在本地更新长驻分支 dev/test/uat，merge feat into current branch，之后 checkout 一个新分支，作为 conflict resolved 分支，推送并合并至远程长驻分支 这个模式简单好懂，且业界流行，最直观的好处是，可以满足以下需求：</description>
      <pubDate>Thu, 21 Apr 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 再论Git Flow</h1>
<h2> 背景</h2>
<p>团队目前使用的 Git 协作模式是：</p>
<ol>
<li>对每个功能建立相应的 feat 分支</li>
<li>上研发、测试、UAT环境时，分别把相应的 feat 分支合并进入长驻 dev/test/uat</li>
<li>如有冲突，则在本地更新长驻分支 dev/test/uat，merge feat into current branch，之后 checkout 一个新分支，作为 conflict resolved 分支，推送并合并至远程长驻分支</li>
</ol>
<p>这个模式简单好懂，且业界流行，最直观的好处是，可以满足以下需求：</p>
<ol>
<li>某 feat 合并至 dev 后，并不想合并至 test</li>
<li>某 feat 合并至 test 后，并不想合并至 uat</li>
</ol>
<p>本文暂且不讨论该交付理念的优劣，毕竟每个团队研发情况、交付理念都不一样。 本文关注的是，在满足上述需求的情况下，是否有更好的分支协作方式。</p>
<h2> 动机</h2>
<p>为什么要寻求更好的方式？因为上述分支协作模式，会导致代码冲突的噩梦：</p>
<ol>
<li>feat -&gt; dev，解决冲突</li>
<li>feat -&gt; test，又要解决冲突</li>
<li>feat -&gt; uat，还要解决冲突</li>
</ol>
<p>正如<a href="https://www.cloudbees.com/blog/pitfalls-feature-branching" target="_blank" rel="noopener noreferrer">此文章</a>所说，“把时间浪费在解决不必要的冲突上”。</p>
<p>再者，功能已经通过测试了，准备上UAT环境时，居然还要解决一大堆曾经解决过的冲突，实在不想接受这种“惊喜”（或者说“惊吓”更合适）——合错代码了怎么办？并且，这么多分支，遗漏了怎么办？这些问题可以解决，但难免有为了解决一个问题，引入更多问题之嫌。</p>
<p>理想中的研发流程是，测试通过后，上 UAT 的体验是平滑的，是不用担心出错的。</p>
<p>为此，本文思考是否存在另一种分支协作的方式。</p>
<h2> 分析</h2>
<p>首先分析一下，“某功能测试通过但不上 UAT”的可操作方法有哪些：</p>
<ol>
<li>要上 UAT 的 feat 分支逐个依次合并至长驻分支，也即当前的做法</li>
<li>在原计划要上的功能的代码集合中，剔除掉相应 feat 的代码，再上 UAT</li>
<li>相应分支再次提交代码，或提交 revert commit，或屏蔽相应的功能及入口，变相达到目的</li>
</ol>
<h3> 剔除代码</h3>
<p>先来看第2种方法。filter by branch，这是最先想到且符合直觉的方式，可惜实际上 Git 并没有此功能。</p>
<p>想“剔除某 feat 分支的代码”，可操作方式如下，更多请<a href="https://www.clock.co.uk/insight/deleting-a-git-commit" target="_blank" rel="noopener noreferrer">参考此文章</a>：</p>
<ol>
<li><code>git rebase</code></li>
<li><code>git cherry-pick</code></li>
</ol>
<p><code>git rebase</code>要求 commit 是连续的，这对于实际不可行，因为集成分支里各个 feat 分支的提交记录掺杂在一起。</p>
<p><code>git cherry-pick</code>是可行的。不过其思路是挑捡想要的 commit，放到目标分支，本质上并不是剔除的逻辑。</p>
<h3> 再次提交</h3>
<p>真正的剔除逻辑，存在于第3种方法中。提交一个 revert commit，就可以把之前的代码干掉了（如果想恢复代码，需要 revert "revert commit")。</p>
<p>觉得 revert 可能会对后续恢复代码造成困扰的话，也可以再提交代码，屏蔽相应功能及入口。这种方式适合于功能入口少，功能本身具有类似开关特性的场景。</p>
<h3> 比较优劣</h3>
<p>要比较上述方案优劣，本文倾向于使用功利主义的最佳实践作为指导思想——认为痛苦存在更多共同点，因而为避免消极而努力。换言之，本文关注的是，哪种方案最令人痛苦，则优先淘汰它。</p>
<p>还有一个指导思想：麻烦、辛苦的事情放前面；前面可以多做，后面期望少做。</p>
<p>当前的方式，存在最难受的问题：解决过的冲突，需要重复地解决。涉及范围：全部分支。涉及人员：所有参与研发的人员，即使他们在别的 feat 分支提交代码。</p>
<p>cherry-pick 依然存在要重复解决冲突的问题，且涉及范围同样为全部分支，但涉及人员减少为单人，因为只需要一个做 cherry-pick 的工作，由其解决 cherry-pick 遇到的冲突（当然，很可能需要他人协助）。</p>
<p>再次提交，冲突的可能性将大大减少，涉及范围：相应的 feat 分支。涉及人员：相应的 feat 分支研发人员。</p>
<p>也即使用再次提交的方案，痛苦将降低至最小。这也是符合直觉的：谁出问题，谁负责。某功能不上线了，这也算是“问题”的一种，则相应的负责人去处理，尽可能不影响到其他人。</p>
<p>至于再次提交是使用 revert 还是屏蔽功能及入口，则具体情况具体分析。</p>
<h2> 实例</h2>
<p>下面举例说明，如何应用上述分析结果。</p>
<h3> 分支模型</h3>
<p>长驻分支： dev/test/uat，分别对应环境：研发/测试/UAT</p>
<p>一个月一次的迭代开始时，都建立相应的 &nbsp;release 分支，命名规则可以：</p>
<ul>
<li>按版本，如： release/v2.15</li>
<li>按上线日期，如：release/04-26</li>
</ul>
<h3> 功能提交</h3>
<p>每个研发人员根据相应功能，从 uat checkout 相应的 feat 分支。</p>
<p>每次需要集成发布时，正常的分支合并操作如下：</p>
<ol>
<li>feat -&gt; dev</li>
<li>feat -&gt; release</li>
<li>release -&gt; test</li>
<li>release -&gt; uat</li>
</ol>
<p>则冲突大多数情况只发生在第前两步，解决之后，后续上测试环境、上 UAT 环境，基本无需担心冲突。</p>
<p>为什么一个 feat 要合并两次？</p>
<p>因为要保证 release 的功能是较为完整的, 至少经过开发人员在 dev 环境的自测。</p>
<p>并且这样也能适应不同的功能分批提测的研发节奏。</p>
<h3> 功能回撤</h3>
<p>当 release 合并至 test 分支后，得到通知，某功能（分支涉及 feat/unwanted）不上 UAT。</p>
<p>则此时，feat/unwanted 相应的研发人员，为了进行功能回滚，操作如下：</p>
<ol>
<li>checkout rollback 分支</li>
<li>进行回滚提交，或 revert，或屏蔽功能入口</li>
<li>请求合并至 UAT（不合并至 release/分支）:</li>
</ol>
<p>后续要恢复功能，在 rollback 分支操作，再合并至 UAT 即可。</p>
<h2> 结论</h2>
<p>通过分析与比较，本文推荐使用“再次提交”的方式，来满足某 feat 分支合进 test，不合进 uat 分支的需求。</p>
<p>这样做，将改动涉及范围减至最小，涉及人员降为单人，大大减少上 UAT 时合并代码的痛苦，达到平稳上线的目的。</p>
]]></content:encoded>
    </item>
    <item>
      <title>操作 Gitlab MR 的命令行工具</title>
      <link>https://levy.vip/git/use-command-line-tool-to-manage-gitlab-merge-request.html</link>
      <guid>https://levy.vip/git/use-command-line-tool-to-manage-gitlab-merge-request.html</guid>
      <source url="https://levy.vip/rss.xml">操作 Gitlab MR 的命令行工具</source>
      <description>操作 Gitlab MR 的命令行工具 背景 为什么开发这个工具？主要解决以下问题： 提测、上 UAT 时，避免漏合代码。 代码冲突时，团队成员不用再问“解决这个冲突要怎么切分支？” 一个 feature 分支要向多个保护分支提交合并请求时，减少烦琐而易错的选取分支的界面操作。 可能会有人问：为什么会漏合代码？当你在某一个迭代需要来回在不同的 feature 分支切换、一个 feature 横跨多个项目，同时你偶尔还要兼顾 bug 修复的时候，你极容易丢失上下文。 并且，不同的 feature 研发进度不一致，可能出现的一种情况是：feature A 只是合并到 test 分支，但　feature B 却已经合并到了 uat。 对此，有人问你代码到底合并了没，你怎么确认？一个个项目去相应的主干分支里查看提交历史吗？就是因为不想再这样做了，这才有了这个工具。</description>
      <pubDate>Thu, 23 Mar 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 操作 Gitlab MR 的命令行工具</h1>
<h2> 背景</h2>
<p>为什么开发这个工具？主要解决以下问题：</p>
<ol>
<li>提测、上 UAT 时，避免漏合代码。</li>
<li>代码冲突时，团队成员不用再问“解决这个冲突要怎么切分支？”</li>
<li>一个 feature 分支要向多个保护分支提交合并请求时，减少烦琐而易错的选取分支的界面操作。</li>
</ol>
<p>可能会有人问：为什么会漏合代码？当你在某一个迭代需要来回在不同的 feature 分支切换、一个 feature 横跨多个项目，同时你偶尔还要兼顾 bug 修复的时候，你极容易丢失上下文。<br>
并且，不同的 feature 研发进度不一致，可能出现的一种情况是：feature A 只是合并到 test 分支，但　feature B 却已经合并到了 uat。<br>
对此，有人问你代码到底合并了没，你怎么确认？一个个项目去相应的主干分支里查看提交历史吗？就是因为不想再这样做了，这才有了这个工具。</p>
<h2> 安装</h2>
<h3> 解压zip</h3>
<p>下载并解压文件:</p>
<ul>
<li><a href="https://r0e715v8ejr.feishu.cn/file/IxH4bYAOkowK08xSid1crXcSnRo" target="_blank" rel="noopener noreferrer">Windows</a></li>
<li><a href="https://r0e715v8ejr.feishu.cn/file/ORa3buA3donF3TxxPVwcHSYnnQb" target="_blank" rel="noopener noreferrer">Linux</a></li>
</ul>
<h3> 安装git bash</h3>
<p>Windows系统才要安装。<br>
如果 git bash 版本不足 2.41.0，最好安装最新版本。</p>
<p>安装地址：<a href="https://gitforwindows.org/" target="_blank" rel="noopener noreferrer">https://gitforwindows.org/</a></p>
<h2> 配置</h2>
<p>新增文件</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>复制以下内容：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>对于该配置的解释，详见后文。</p>
<h3> gitlab_token</h3>
<p>先获取 gitlab token，操作如下：</p>
<ol>
<li>打开Gitlab，右上角点击个人头像</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154008266.png" alt="0a8ff2ce0d86d685fd2b5283c40871d9.png" tabindex="0"><figcaption>0a8ff2ce0d86d685fd2b5283c40871d9.png</figcaption></figure>
<ol start="2">
<li>点击左侧边栏</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154013693.png" alt="f1151c580d5672d187ca38699e9c2013.png" tabindex="0"><figcaption>f1151c580d5672d187ca38699e9c2013.png</figcaption></figure>
<ol start="3">
<li>勾选全部权限，并确认生成 token</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154016668.png" alt="790ed16e056c26d79f35b7ff4c072c8f.png" tabindex="0"><figcaption>790ed16e056c26d79f35b7ff4c072c8f.png</figcaption></figure>
<h3> codebases</h3>
<p>适用于多基线的场景。</p>
<p>如产品默认有三个环境，分别对应三条分支 dev、test、master。<br>
同时，又有定制化需求，专门为某一客户进行源码改动，同样有三个环境，则可能出现的配置如下：</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 环境变量</h3>
<p>把 mr 可执行文件所在目录设置到环境变量中：</p>
<ol>
<li>查找"环境变量"</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154020270.png" alt="157a114acdf00def50ae774b4d68e004.png" tabindex="0"><figcaption>157a114acdf00def50ae774b4d68e004.png</figcaption></figure>
<ol start="2">
<li>点击"环境变量"</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154023459.png" alt="6a99dd2754b75348523a388d36067bd9.png" tabindex="0"><figcaption>6a99dd2754b75348523a388d36067bd9.png</figcaption></figure>
<ol start="3">
<li>找到 Path，点击"编辑"</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154026703.png" alt="d1335d05ddefa6aa33006e9f24f3254f.png" tabindex="0"><figcaption>d1335d05ddefa6aa33006e9f24f3254f.png</figcaption></figure>
<ol start="4">
<li>点击"新增"，再点击"浏览"，找到最里层的 mr 目录</li>
</ol>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154030135.png" alt="6df5b86a1f91452414382718983e0e22.png"><br>
（上图是示例，具体路径根据自己的情况而定）</p>
<p>重新打开 git bash 即可生效，记得一定要重新打开！</p>
<p>注：如果是 Linux，那很简单，修改 ~/.bashrc</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> IDEA</h3>
<p>该步骤选填，适用于 JetBrains 系列产品，想在 IDEA 的终端中也使用 mr 命令时可配置。</p>
<p>File -&gt; Settings -&gt; Tools -&gt; Terminal：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154033353.png" alt="image.png"></p>
<h2> 使用</h2>
<p>可以不带参数运行，查看支持的命令：mr<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154109989.png" alt="image.png"></p>
<h3> 创建MR</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>提示输入要提交 MR 的源分支，按回车使用当前分支：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154113289.png" alt="image.png"><br>
创建成功：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154116622.png" alt="ae115431acc7c0b9275c158fb99e3eaa.png"></p>
<p>可以处理同名项目的情况：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/dealing-with-same-name-projects.png" alt="dealing-with-same-name-projects.png"></p>
<p>MR 不会重复创建：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154120100.png" alt="ab7d3ea5177726bbab0ccbcd870d0044.png"></p>
<p>分支没有代码更新时，也不会创建 MR：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154123348.png" alt="147f745360164598c78c3de8808af1d2.png"></p>
<h3> 查看MR</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>可以用来查看自己有哪些MR未合并。注意：只显示自己创建的。</p>
<ul>
<li>如果可以合并，显示 [ok]</li>
<li>如果有冲突，显示 [conflict]</li>
</ul>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154126283.png" alt="85ac4ee87e49965270f502ef830bf619.png" tabindex="0"><figcaption>85ac4ee87e49965270f502ef830bf619.png</figcaption></figure>
<h3> 合并MR</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>{mr_url} 的值可以根据以下方式来获取：</p>
<ol>
<li>create 命令成功后的输出</li>
<li>list 命令的输出</li>
<li>gitlab web界面上 MR 的 url</li>
</ol>
<p>合并前会有确认提示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154129605.png" alt="image.png"></p>
<p>可以取消，防止误合并：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154133045.png" alt="image.png"></p>
<h3> 冲突处理</h3>
<p>解决冲突，切换分支，是很麻烦的事情，故本工具为解决冲突提供了一些辅助功能。</p>
<p>注意：命令行只做拉取代码、切合分支等必要操作，冲突的解决仍需要人工介入，工具不会自动合并代码的。</p>
<p>合并冲突状态的MR：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>出现提示，是否自动切换分支为解决冲突作准备：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154136190.png" alt="image.png"><br>
当然在此之前，要保证工作目录是干净的，如果有修改未提交，会中止切换分支操作：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154139322.png" alt="image.png"></p>
<blockquote>
<p>可以使用 <code>git stash</code>保存修改，合并冲突后，再 <code>git stash pop</code></p>
</blockquote>
<p>命令执行成功时，会切换到 <code>conflict/</code> 开头的分支。<br>
此时，打开 IDE 或 Git 管理工具，根据提示把相应的分支合并到 <code>conflict/</code> 分支即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154143622.png" alt="image.png"></p>
<p>以 IDEA 为例：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154146323.png" alt="image.png"></p>
<p>解决冲突后，再切回命令行，此时有两种选择：</p>
<ol>
<li>创建 MR，适用于自己没有权限合并的场景</li>
<li>合并 MR，适用于自己有权限合并的场景</li>
</ol>
<p>如果是创建，再次执行 create 命令即可：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154149211.png" alt="image.png"><br>
创建的 MR 合并时会自动删除 <code>conflict/</code> 分支。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154152000.png" alt="image.png"></p>
<p>如果是合并，同样再次执行 merge 命令即可，此时不用带参数：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154155089.png" alt="image.png"><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154158111.png" alt="52fee749bc6d270f9ccab3eb0e04208b.png"></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1689154008266.png" type="image/png"/>
    </item>
    <item>
      <title>IDEA常见问题与解决方案</title>
      <link>https://levy.vip/java/Resolving-Common-Problems-in-IntelliJ-IDEA.html</link>
      <guid>https://levy.vip/java/Resolving-Common-Problems-in-IntelliJ-IDEA.html</guid>
      <source url="https://levy.vip/rss.xml">IDEA常见问题与解决方案</source>
      <description>IDEA常见问题与解决方案 启动参数过长 Error running OrderStartupApplication. Command line is too long. Shorten the command line and rerun. 解决方案： 编辑 .idea/workspace.xml 找到 PropertiesComponent 添加： 或者这样：</description>
      <pubDate>Fri, 25 Nov 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> IDEA常见问题与解决方案</h1>
<h2> 启动参数过长</h2>
<p>Error running OrderStartupApplication. Command line is too long. Shorten the command line and rerun.<br>
解决方案：</p>
<ol>
<li>编辑 .idea/workspace.xml</li>
<li>找到 <code>PropertiesComponent</code></li>
<li>添加：</li>
</ol>
<p>或者这样：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1691568843067-69847e70-987e-4263-81f3-7f19c3acccc5.png" alt=""></p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 设置JDK版本</h2>
<p>相关报错：</p>
<ul>
<li><a href="https://blog.csdn.net/qq_32452623/article/details/106141126" target="_blank" rel="noopener noreferrer">java: Compilation failed: internal java compiler error</a></li>
<li>Cannot resolve jdk.tools:jdk.tools:1.7</li>
</ul>
<p>解决方案如下。</p>
<p>1.先确保已安装 jdk。</p>
<p>2.修改运行设置<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1637063344496-6b8f60c5-c444-4f77-a404-9b8ca8d7a9bb.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1637063379719-7c3d4323-5d77-44c4-8c49-7d79c4d61865.png" alt=""><br>
3.修改外部依赖设置<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1645614452333-3f5d2763-e7a1-42d8-a3ea-b73cb664e6a1.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1645614476794-b37230c5-958a-459d-8dce-ecb5cb839d3f.png" alt=""></p>
<h2> lombok 编译报错</h2>
<p>前提：<a href="https://blog.csdn.net/weixin_42440768/article/details/107999786" target="_blank" rel="noopener noreferrer">lombok 有maven依赖后，还要安装IDE插件</a></p>
<p>相关报错：<br>
<a href="https://stackoverflow.com/questions/66801256/java-lang-illegalaccesserror-class-lombok-javac-apt-lombokprocessor-cannot-acce" target="_blank" rel="noopener noreferrer">class lombok.javac.apt.LombokProcessor (in unnamed module @0x29f3e3c7) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x29f3e3c7</a>：</p>
<p>解决方案：找到相应的 pom.xml，更新依赖版本（如果没有，则添加依赖）</p>
<div class="language-text line-numbers-mode" data-ext="text"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当然，还要确保项目 JDK 版本正确<a href="https://blog.csdn.net/qq_32452623/article/details/106141126" target="_blank" rel="noopener noreferrer"><br>
</a></p>
<h2> 设置启动参数</h2>
<p>Run -&gt; Edit Configurations<br>
注意是 VM options</p>
<p>注入环境变量：spring.profiles.active=local<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1698212254651-9ffbcfab-7e1a-45c0-ae93-2ef2ad68cf11.png" alt=""></p>
<p>也可以设置 VM options，不过要带上 -D：-Dspring.profiles.active=local<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1698212412181-3374a830-1693-4db2-95eb-b314e6313517.png" alt=""></p>
<h2> 栈溢出</h2>
<p>maven build "Exception in thread "main" java.lang.StackOverflowError"</p>
<p>-Xss40m<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1669357898562-420a3ceb-3dfd-4b9c-93fa-8884edc8b231.png" alt=""></p>
<p>不是maven的编译选项在下面<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1669357950353-4cc40792-1e54-400e-b3c8-aab28b71df64.png" alt=""></p>
<h2> 内存不足</h2>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1637063639112-cf83394f-5c10-4eb6-9946-8f10cf901f62.png" alt=""><br>
-Xmx4011m</p>
<hr>
<p>相关报错：<br>
java: java.lang.OutOfMemoryError: GC overhead limit exceeded</p>
<p>解决方案：需要进行如图所示修改设置<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1639039051050-9a0039c3-1087-4701-8ee9-0d61994bf2ca.png" alt=""></p>
<h2> 热加载</h2>
<p>相关文章：<a href="https://cloud.tencent.com/developer/article/1683029" target="_blank" rel="noopener noreferrer">https://cloud.tencent.com/developer/article/1683029</a></p>
<p>提示：不用追求自动重新编译，手动按 build 即可。</p>
<h2> 终端加载环境变量</h2>
<p>相关问答：<a href="https://stackoverflow.com/questions/36592226/bashrc-not-sourced-on-intellij-ideas-terminal/59138750#59138750" target="_blank" rel="noopener noreferrer">https://stackoverflow.com/questions/36592226/bashrc-not-sourced-on-intellij-ideas-terminal/59138750#59138750</a></p>
<p>注意两点：</p>
<ol>
<li>shell 命令带上 -i</li>
<li>根据 shell 的版本，使用 .bashrc 或 .zshrc</li>
</ol>
<h2> 添加外部jar作为依赖</h2>
<p>如下图所示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1669256297330-7985774e-3d1b-4d8b-af90-5b179b1fc825.png" alt=""><br>
打开相应文件夹，选中jar即可。</p>
<h2> 文件找不到——依赖冲突</h2>
<p>相关报错：<code>nested exception is java.io.FileNotFoundException</code><br>
这一般是 jar 包冲突。</p>
<p>首先确保 pom.xml 的修改已生效，再利用 Maven Helper 插件，寻找冲突的依赖，根据报错信息，把不想的包 exclude 掉，重新加载 pom.xml。</p>
<p>如果报错的包根本不在冲突列表里，也有可能是以下情况：</p>
<ul>
<li>版本不对， 则 google 一下相关报错，设置成正确的版本</li>
<li>引入了多余的包，执行了不想要的逻辑</li>
</ul>
<p>exclue掉：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1666767522281-08333ca8-f4fa-4965-8e61-65b4da2f3524.png" alt=""></p>
<p>重新加载：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1666767567101-8308c02c-a792-4fbd-9198-6edf2a514e8c.png" alt=""></p>
<h2> 自动import</h2>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1659925756116-626b591f-2be4-4bb5-90c8-91f65989fc2b.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 文件乱码</h2>
<p>如图所示，根据情况修改即可：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1641976626307-9d57d76f-eb57-4f57-8660-c77813485d5a.png" alt=""></p>
<h2> autowired 提示变量未赋值</h2>
<p>这是<a href="https://stackoverflow.com/a/44670144/6759562" target="_blank" rel="noopener noreferrer">因为我使用的是社区版</a>，需要<a href="https://stackoverflow.com/a/62437991/6759562" target="_blank" rel="noopener noreferrer">手动设置下</a>：<br>
<img src="https://i.stack.imgur.com/3bSYa.png#from=url&amp;id=Wxizo&amp;originHeight=368&amp;originWidth=1858&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;status=done&amp;style=none&amp;title=" alt=""><br>
该方法可以放心使用。<br>
虽然说的是 suppress unsed warning，其实是 suppress never assigned warning， unsed warning 还是会生效的。</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1691568843067-69847e70-987e-4263-81f3-7f19c3acccc5.png" type="image/png"/>
    </item>
    <item>
      <title>Maven常见问题与解决方案</title>
      <link>https://levy.vip/java/Resolving-Common-Problems-in-Maven.md.html</link>
      <guid>https://levy.vip/java/Resolving-Common-Problems-in-Maven.md.html</guid>
      <source url="https://levy.vip/rss.xml">Maven常见问题与解决方案</source>
      <description>Maven常见问题与解决方案 运行 class 找不到主类 maven compile 得到 class 文件后 cd /my-app/target/com/mycompany/app java App</description>
      <pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Maven常见问题与解决方案</h1>
<h2> 运行 class 找不到主类</h2>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>得到 class 文件后</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>报错：</p>
<blockquote>
<p>错误: 找不到或无法加载主类 App<br>
原因: java.lang.NoClassDefFoundError: com/mycompany/app/App (wrong name: App)</p>
</blockquote>
<p>这是因为主类并非在默认包下，故需要在正确的路径下调用全限定名。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 运行 jar 找不到主类</h2>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>打出 jar 后，运行失败，因为主清单找不到主类。这是因为缺少了打包配置</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 编译时找不到主类</h2>
<p>表现：Maven 有相应的 jar，IDEA 能自动 import，编译时却报找不到类 <code>NoClassDefFoundError</code></p>
<p>实例：flink-quickstart-java 项目就是如此。</p>
<p>原因：这跟 Maven 依赖的 score 有关，因为 pom.xml 对依赖的 scope 定义为 provided，默认时编译不会去找相应的依赖。关于 maven scope 的知识点，<a href="https://www.baeldung.com/maven-dependency-scopes" target="_blank" rel="noopener noreferrer">点击这里</a>。</p>
<p>解决方案：</p>
<ol>
<li>build configuration</li>
<li>modify options</li>
<li>把 provided 加入 classpath</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1668066163479-43ea1144-f764-4ec0-b5ca-823b8efccae8.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 设置Maven目录</h2>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1670227320589-edf93b93-25d6-4992-9fe1-528218537ecf.png" alt=""><br>
尽管IDEA可以设置 local repository，但实际上还是以 settings.xml 的配置为主，注意检查路径是否正确</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 无法识别 Maven 项目</h2>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1668149177501-37c6b4d2-1674-4ab2-b7c7-f7774ab90e12.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1668149163603-a59d7ffe-7236-4756-8add-a55c1d282754.png" alt=""></p>
<p>此时代码的方法、变量不能跳转到定义处，无法编译、找不到主类。</p>
<p>表面上看，这是没有把源码目录设置为 src root，但实际上，这是 pom.xml 解析缓存或Maven依赖下载失败后的结果。</p>
<p>这里又分两种情况：</p>
<ol>
<li>子模块不能被识别</li>
<li>主模块不能被识别</li>
</ol>
<p>如果是第1种情况，添加了子模块不能正确识别，删除 <code>&lt;module&gt;my-new-submodule&lt;/module&gt;</code> 这一行，重新加载 pom.xml；再重新添加这一行，然后重新加载 pom.xml 即可。</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果是第2种情况，笔者曾遇到的场景是： 因为把 IDEA 下载依赖的进程杀死了而导致的。 解决方案：删除项目，重新创建。</p>
<h2> 使用了不想要的镜像源</h2>
<p>有可能第三方依赖包指定了第三方镜像源，而该镜像源不可用、或网络很慢，此时想避免使用该镜像源。</p>
<p>解决方案：观察日志，确认第三方镜像源的名字，修改 ~/.m2/settings.xml，拦截其请求</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>以上配置会拦截所有指向 Maven中央仓库、third-party-central 的请求，使用 my-central 下载依赖。</p>
<h2> 下载 jar 失败</h2>
<p>如果因为网络问题， mvn 命令安装依赖失败（如下载某个依赖卡死），可以试试手动下载。</p>
<p>解决方案：</p>
<ol>
<li>点击相应的 jar 包链接，使用浏览器下载</li>
<li>观察 jar 包的下载链接，把下载到的 jar 复制到相应的 ~/.m2 子目录下</li>
<li>重新执行 mvn install</li>
</ol>
<h2> 私服认证401</h2>
<p>下载私服依赖时，报了 401。</p>
<p>解决方案：</p>
<ol>
<li>检查 ~/.m2/settings.xml，找到 server 选项，确保设置了相应的用户名与密码</li>
<li>点击私服链接，输入用户名与密码</li>
<li>如果上述步骤不能正确执行，说明设置有误，需要更正设置；如果上述步骤正常，有可能是 IDEA 抽风，建议：
<ol>
<li>删除项目，重新克隆</li>
<li>手动下载 jar，放到~/.m2 目录</li>
</ol>
</li>
</ol>
<h2> 避免缓存</h2>
<p>pom.xml 作好设置</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>或命令行强制不使用依赖：</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 参考资料</h2>
<p>官网：<a href="https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html" target="_blank" rel="noopener noreferrer">https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html</a><br>
书籍：《Maven实战》</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1668066163479-43ea1144-f764-4ec0-b5ca-823b8efccae8.png" type="image/png"/>
    </item>
    <item>
      <title>避免密码明文传输</title>
      <link>https://levy.vip/java/avoid-sending-password-in-plaintext.html</link>
      <guid>https://levy.vip/java/avoid-sending-password-in-plaintext.html</guid>
      <source url="https://levy.vip/rss.xml">避免密码明文传输</source>
      <description>避免密码明文传输 说明 密码加密是很常见的安全性需求，但由于涉及前后端，前后端分离的情况下，开发人员容易只关心自己熟悉的领域，最终导致“知道要加密，实际还是没明文”的情况发生。 本文分享实际可运行的前后端代码，以减轻大家实现密码密文传输的负担。 流程说明：前端加密，后端解密。 当然，数据库存储的肯定是密文。这里后端解密的意思是：需要使用密码的时候，如获取数据库的连接，由后端解密后使用。</description>
      <pubDate>Tue, 24 Oct 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 避免密码明文传输</h1>
<h2> 说明</h2>
<p>密码加密是很常见的安全性需求，但由于涉及前后端，前后端分离的情况下，开发人员容易只关心自己熟悉的领域，最终导致“知道要加密，实际还是没明文”的情况发生。</p>
<p>本文分享实际可运行的前后端代码，以减轻大家实现密码密文传输的负担。</p>
<p>流程说明：前端加密，后端解密。</p>
<p>当然，数据库存储的肯定是密文。这里后端解密的意思是：需要使用密码的时候，如获取数据库的连接，由后端解密后使用。</p>
<!-- more -->
<h2> 前端代码</h2>
<p>记得安装相应的 npm 模块。</p>
<div class="language-typescript line-numbers-mode" data-ext="ts"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 后端代码</h2>
<p>maven:</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>源码：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>单元测试：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
    </item>
    <item>
      <title>检查名字是否重复</title>
      <link>https://levy.vip/java/check-if-name-exists.html</link>
      <guid>https://levy.vip/java/check-if-name-exists.html</guid>
      <source url="https://levy.vip/rss.xml">检查名字是否重复</source>
      <description>检查名字是否重复 检查名字是否重复是很常用的业务需求，本文推荐一种更省心、更少bug的做法。</description>
      <pubDate>Wed, 18 Oct 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 检查名字是否重复</h1>
<p>检查名字是否重复是很常用的业务需求，本文推荐一种更省心、更少bug的做法。</p>
<!-- more -->
<h2> 推荐做法</h2>
<p>借助数据库的来实现，执行以下语句：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>然后，在程序里添加全局异常处理类：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>示例返回：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1697616346892-cea4b6c7-1966-4dc5-84c6-d901eeeabd65.png#averageHue=%23fcfbfb&amp;clientId=u791d113b-6ee5-4&amp;from=paste&amp;height=146&amp;id=u5e5b9578&amp;originHeight=146&amp;originWidth=575&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=11093&amp;status=done&amp;style=none&amp;taskId=u48e1cd4e-70db-4617-be80-0ec3828b522&amp;title=&amp;width=575" alt=""></p>
<h2> Why</h2>
<p>为什么这样做呢？因为这样只需要改一次代码。后续再有类似的需求时，只需要增加 SQL 即可，不需要再修改码、不需要重新编译、构建、部署。</p>
<p>如果选择修改代码，则每次至少要修改两个地方：新增、修改都要处理。</p>
<p>示例代码如下：</p>
<ul>
<li>检查是否存在</li>
</ul>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>新增接口</li>
</ul>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>修改接口</li>
</ul>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>实践的经验表明：改得多，错的多！</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1697616346892-cea4b6c7-1966-4dc5-84c6-d901eeeabd65.png#averageHue=%23fcfbfb&amp;clientId=u791d113b-6ee5-4&amp;from=paste&amp;height=146&amp;id=u5e5b9578&amp;originHeight=146&amp;originWidth=575&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=11093&amp;status=done&amp;style=none&amp;taskId=u48e1cd4e-70db-4617-be80-0ec3828b522&amp;title=&amp;width=575" type="image/"/>
    </item>
    <item>
      <title>Excel处理常用实践</title>
      <link>https://levy.vip/java/common-practices-for-handling-excel.html</link>
      <guid>https://levy.vip/java/common-practices-for-handling-excel.html</guid>
      <source url="https://levy.vip/rss.xml">Excel处理常用实践</source>
      <description>Excel处理常用实践 基础知识 导入需要用到对象，MultipartFile。 @PostMapping(&amp;quot;/import&amp;quot;) public boolean importLicense( @RequestParam(&amp;quot;file&amp;quot;) MultipartFile file, @RequestParam(&amp;quot;tenantId&amp;quot;) @NotBlank String tenantId, ) { return true; }</description>
      <pubDate>Tue, 05 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Excel处理常用实践</h1>
<h2> 基础知识</h2>
<p>导入需要用到对象，MultipartFile。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>导出需要用到对象：HttpServletResponse。作为 Controller 的最后一个入参即可，框架会自动注入。<br>
也可以从请求上下文获取：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>同时注意设置响应头：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 应用框架</h2>
<h3> 导出</h3>
<p>EasyExcel 做 Excel 导出还是挺方便的。</p>
<p>先建立Java实体与Excel表格内容的映射：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>然后收集数据，用下面的语句导出：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 导入</h3>
<p>EasyExcel 处理导入，体验就没那么丝滑了。</p>
<p>可以先看下示例代码：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>设计思路是挺优雅的：事件驱动，把业务逻辑放入这个 Listener 里。</p>
<p>但实践起来就不太优雅了：</p>
<ol>
<li>无法利用 Spring 进行依赖管理，需要手动在构造器中注入业务对象。对于这一点，我表示很难受。我相信，当业务变得复杂，这个构造器要接收 9 个参数的时候，大部分人都会难受。</li>
<li>需要维护横跨回调函数的“全局变量”。当业务复杂后，势必要引入更多的类成员变量，在 invoke/invokeHead/doAfterAllAnalysed 等回调函数中出现多次，对这几个回调函数而言，类成员变量就是全局变量。代码是可以实现，但个人不倾向于这种做法。另外，这也给我一种”梦回前端“的感觉，我对这种感觉不持有积极态度。</li>
</ol>
<p>当然上面仅仅是吐槽，又不是不能用。业务不复杂的话，直接用起来就完事了。</p>
<h2> 常见问题与解决方案</h2>
<h3> 浏览器下载</h3>
<p>虽然说 GET 请求可以让浏览器直接下载文件，Postman 也验证过此方案，但很可能实际会让前端采取另一种实现方案：先进行 Ajax 请求，再利用返回的数据创建 Blob 对象，最后才下载。</p>
<p>为什么不直接让浏览器下载，而要前端转一层呢？大概率是因为鉴权问题——因为浏览器 GET 请求不能带上授权相关的请求头，小概率原因是想前端显示 loading 状态。</p>
<p>前端多转一层，很可能转出问题，排查起来，既要 debug 接口，又要 debug 前端代码，吃力不讨好。</p>
<p>对此，我推荐的实践是：请求 url 带上 token 信息，如 /download?token=xxx，让后端对于下载接口特殊处理，以更前端少做一次处理，直接让浏览器下载文件即可。</p>
<h3> 上传文件大小限制</h3>
<p>导入本质是文件上传，服务端会生成临时文件，当文件过大时，需要修改相关设置。</p>
<p>以下是 Spring 的相关配置。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 缺少字体</h3>
<p>Excel 的导出需要字体文件。</p>
<p>缺少字体时，可能会报错：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>为什么会缺少字体呢？一个经典的例子就是，使用了过于精简的基础镜像来打包应用，如使用 busybox 就会报错：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>解决方案就是换一个更“丰满”一点的镜像。</p>
<p>但如果没有导出的内容没有格式要求，其实推荐优先使用 CSV。因为 CSV 不需要字体，而又与 Excel 兼容。</p>
<h3> 序列化失败</h3>
<p>一般应用程序都会对请求参数进行日志打印，当处理文件时，可能会遇到以下错误：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>找到相应代码：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>问题出在第 12 行，上传的文件 Multipart 对象被序列化时报错了。但这不应认为是 fastjson 问题——就算换也 Jackson，默认情况也是会报错的。</p>
<p>这里具体问题具体分析。笔者的场景，其实只是要打印参数，并不需要序列化文件对象，因此，通过跳过序列化 Multipart 对象解决此问题，示例代码如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
    </item>
    <item>
      <title>奇技淫巧：在没有源码的情况下，把 snapshot 转成 release 包</title>
      <link>https://levy.vip/java/how-to-convert-snapshot-into-release-jar-without-source-code.html</link>
      <guid>https://levy.vip/java/how-to-convert-snapshot-into-release-jar-without-source-code.html</guid>
      <source url="https://levy.vip/rss.xml">奇技淫巧：在没有源码的情况下，把 snapshot 转成 release 包</source>
      <description>奇技淫巧：在没有源码的情况下，把 snapshot 转成 release 包 背景 项目中依赖了一个旧的 snapshot.jar，有人提出要求必须使用 release.jar，不能使用 snapshot。 问题来了，该 jar 所属的源码不知所踪，那还怎么发布 release.jar 呢？这就是本文要解决的问题。</description>
      <pubDate>Sat, 12 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 奇技淫巧：在没有源码的情况下，把 snapshot 转成 release 包</h1>
<h2> 背景</h2>
<p>项目中依赖了一个旧的 snapshot.jar，有人提出要求必须使用 release.jar，不能使用 snapshot。</p>
<p>问题来了，该 jar 所属的源码不知所踪，那还怎么发布 release.jar 呢？这就是本文要解决的问题。</p>
<!-- more -->
<h2> 下载</h2>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748114074-8149044d-a066-4069-9bbd-7124bede3686.png" alt=""><br>
首先我们登录仓库，输入　ArtifactId，找到对应的 snapshot jar 包，并点击进入详情。</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748125239-9f38e77c-e2a9-4cb9-a5ae-663d1cbdbc5a.png" alt=""><br>
找到关键的三个 jar:</p>
<ul>
<li>x.jar</li>
<li>x-sources.jar</li>
<li>x.pom</li>
</ul>
<p>分别点击进入详情<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748130574-143b17a9-4ecc-4543-8802-d51b14f43651.png" alt=""><br>
依次点击　Path，下载到本地。</p>
<h2> 修改</h2>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748138424-a8260207-8884-4054-8b86-c247b3766a3b.png" alt=""><br>
首先修改名字，如图所示。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748145414-f10b3b8a-9f57-493e-ae78-978b80e41dcd.png" alt=""><br>
再使用任意工具解压 x.jar，提取出两个文件夹。进入　META-INF</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748152420-1e600d70-ff87-4962-844f-021fe5288aa2.png" alt=""><br>
修改　MANIFEST.MF，把里面的 snapshot 字符串去掉（如果有的话）</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748159789-a1ebe8eb-231f-435c-aabb-0c2fb06f93db.png" alt=""><br>
再点击　maven　文件夹</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748164871-77ab05e9-480f-4afb-9f48-6dba9b89a49a.png" alt=""><br>
修改　pom.xml、pom.properties　文件，把里面的 snapshot 字符串去掉。</p>
<h2> 上传</h2>
<p>把前面解压出来的文件重新打包成 jar</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>然后上传仓库<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748169575-27ed04cd-a4c5-4956-b8fc-9e7acb9b7f42.png" alt=""><br>
在Upload中，点击　maven-releases</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748175045-badb4fa7-8d30-45ad-8707-d096da6a28ec.png" alt=""><br>
把三个文件添加上去，点击　Upload。</p>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748258011-2fef169f-82c0-41c4-a7f7-d7bca0065b25.png" alt=""><br>
可以看到，新的 release 版本的 jar 包已经在仓库中了，可以被安装使用了。</p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1691748114074-8149044d-a066-4069-9bbd-7124bede3686.png" type="image/png"/>
    </item>
    <item>
      <title>集合命名推荐</title>
      <link>https://levy.vip/java/recommend-practices-for-collections-naming-convention.html</link>
      <guid>https://levy.vip/java/recommend-practices-for-collections-naming-convention.html</guid>
      <source url="https://levy.vip/rss.xml">集合命名推荐</source>
      <description>集合命名推荐 概述 建议给常用集合类的变量命名时，后缀带上相应的集合信息，以提高可读性。 当然，在此之前要回答一个问题：当把鼠标放到变量上面时，IDE 会提示变量的类型，为什么还要在命名上做文章？ 这是因为，有时并不在 IDE 上阅读代码，比如进行 GitHub 或 GitLab 进行 code review，此时无法获得提示，需要通过命名的规范来帮助理解。</description>
      <pubDate>Wed, 01 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 集合命名推荐</h1>
<h2> 概述</h2>
<p>建议给常用集合类的变量命名时，后缀带上相应的集合信息，以提高可读性。</p>
<p>当然，在此之前要回答一个问题：当把鼠标放到变量上面时，IDE 会提示变量的类型，为什么还要在命名上做文章？<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654049668320-c53d0ef6-4063-4163-aa3e-2c304af4e39a.png" alt=""></p>
<p>这是因为，有时并不在 IDE 上阅读代码，比如进行 GitHub 或 GitLab 进行 code review，此时无法获得提示，需要通过命名的规范来帮助理解。</p>
<h2> List</h2>
<p>List 的变量，一般以 List 或 s 结尾， 如 idList 或 ids。这点易于理解，大家也容易遵守。</p>
<p>坏的示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>第一眼看到这代码的时候，不知道读者是什么反应？</p>
<p>按照习惯，nodeType 通常要么是字符串、数字、或枚举，但上述居然能调用 forEach 方法？我不禁愣了一下，赶紧去看了下定义，才发现原来是List。</p>
<p>好的示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> Set</h2>
<p>参考List，在变量后面加 Set 即可。</p>
<h2> Map</h2>
<p>Map 的变量命名是值得重点关注，因为很容易造成差可读性的重灾区。</p>
<p>Map 的变量，推荐根据 key 与 value 来命名。规则表达式为：${key} + To + ${value} + Map，如 idToNameMap。<br>
其中：</p>
<ul>
<li>可以玩一下“文字游戏”，把 To 写成 2（就像把 For 写成 4，这种 word play 是可以接受的），即 id2NameMap</li>
<li>如果名字够清晰或已经很长，Map 可以省略，如 id2Name</li>
</ul>
<p>为什么推荐这样命名？我们先来看一则案例，看一看经典的 Map&lt;String, String&gt; 在实际编码中，命名是如何造成理解上的困难的。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><code>getTableIdMap</code>核心实现如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><code>replaceNodeId</code>核心实现如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>不知读者是否已经晕了？反正我是一头雾水。可能以为是因为我删减了很多代码导致的？恰恰相反，实际代码还有更多的逻辑判断，我已经抽出了核心部分，不需要被其他逻辑干扰了。</p>
<p>上面的代码带来的疑问有：<br>
疑问1：都是 <code>Map&lt;String, String&gt;</code>，<code>tableIdMaps</code> 与 <code>tableMaps</code> 有什么区别，它们存储的到底是什么？从类型上看，也不像是 id -&gt; table 的映射啊。<br>
疑问2：<code>tableMaps.put(tableNode.getNodeId(), tableNodeId);</code>  这个 <code>tableNode.getNodeId()</code> 不是等于 <code>tableNodeId</code>吗？<br>
疑问3：<code>String nodeId = tableMaps.get(tableNode.getNodeId()); </code>根据 nodeId 拿到 nodeId？</p>
<p>到这里已可以猜到，<code>tableMaps</code>里的 key 与 value 肯定不是单纯的 nodeId 的意思，但这并没有什么帮助，因为我们还是不知道 <code>tableMaps</code>  key -&gt; value 映射的到底是什么。</p>
<p>我们来看修改变量名之后，上述代码的效果。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>现在是不是好懂很多了：</p>
<ul>
<li>str2TableIdMap 存储的 str -&gt; tableId 的映射, 其中 str 是由某种规则拼接而成的字符串，具体规则封装在了 <code>getTableNodeId</code>这个函数里，我们暂时可以不用关心</li>
<li>nodeId2TableIdMap 存储的是 nodeId -&gt; tableId 的映射</li>
</ul>
<p>仅仅修改变量名，可读性就有大大提高，效果立竿见影！</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654049668320-c53d0ef6-4063-4163-aa3e-2c304af4e39a.png" type="image/png"/>
    </item>
    <item>
      <title>根据时间范围查询推荐实践</title>
      <link>https://levy.vip/java/recommend-practices-for-query-by-date-range.html</link>
      <guid>https://levy.vip/java/recommend-practices-for-query-by-date-range.html</guid>
      <source url="https://levy.vip/rss.xml">根据时间范围查询推荐实践</source>
      <description>根据时间范围查询推荐实践 背景 不敢说是最佳实践，因为受限于特定技术、框架，并且带上了个人偏好。 虽然原理简单，但细节很多，不想每次搞来搞去，因此还是有记录的价值。 本文用到的技术栈为：MySQL、MyBatis、Java 8、Jackson</description>
      <pubDate>Tue, 12 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 根据时间范围查询推荐实践</h1>
<h2> 背景</h2>
<p>不敢说是最佳实践，因为受限于特定技术、框架，并且带上了个人偏好。</p>
<p>虽然原理简单，但细节很多，不想每次搞来搞去，因此还是有记录的价值。</p>
<p>本文用到的技术栈为：MySQL、MyBatis、Java 8、Jackson</p>
<!-- more -->
<h2> 需求</h2>
<p>删除某个时间段以前的日志。类似于消除浏览记录：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1694499288408-aaec3b0c-9d06-43c1-849b-7af4ec9eb01a.png" alt=""></p>
<h2> 分析</h2>
<p>上图只是UI示例，为了适应UI的变化，最好把接口设置成接收两个参数：</p>
<ul>
<li>开始时间</li>
<li>结束时间</li>
</ul>
<p>如果UI如上图所示，则选择过去 7 天时，开始时间就是 1970 年 1 月 1 日，结束时间就是过去第 7 天。</p>
<h2> 实现</h2>
<h3> MySQL</h3>
<p>对于 MySQL，推荐使用，因为简单直观，且方便：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>另一种方式：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>上面是时间精确到秒的设计思路，逻辑上没有问题，但体验会有问题：如果是查询某个时间范围的日志还好，精确一点；现在需要是删除日志，谁还关心到秒啊！</p>
<p>通常人们只会关心到天，则考虑一种情况：开始时间与结束时间是同一天，下面的语句无法删除任何记录：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>那么现在，就是分歧点了，有两种方案：</p>
<ol>
<li>程序为开始时间、结束时间填充时分秒：</li>
</ol>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><ol start="2">
<li>结束时间 +1 天：</li>
</ol>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>两种方案都是可以行，我推荐的是第二种。并且，由后端处理，不需要前端拼接字符串。</p>
<blockquote>
<p>注：结束时间 +1 的原因是，实际存储的的 created_time 是带有时分秒的，而用户选择时，只精确到日，这就相当于时分秒的值为0。如果结束时间不加1，则结束时间当天的记录都无法匹配到。</p>
</blockquote>
<h3> MyBatis</h3>
<p>如果使用的是 <code>&gt;=</code> 这种要SQL写法，MyBatis 就需要转义：</p>
<ul>
<li><code>&gt;</code> 转成 <code>&amp;gt;</code></li>
<li><code>&lt;</code> 转成 <code>&amp;lt;</code></li>
</ul>
<p>这就是不推荐这种写法的原因之一。</p>
<h3> LoxalDate</h3>
<p>都已经 Java 8 了，就不要使用 java.util.Date 了，使用 java.time.LocalDate 吧。方便应对后续的时间操作。</p>
<p>结束时间+1天，非常简单：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>记得在 Controller 对时间字段加注解：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>否则会报错：</p>
<blockquote>
<p>Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDate' for property 'startDate'; nested exception is org.springframework.core.convert.ConversionFailedException:</p>
</blockquote>
<h3> Jackson</h3>
<p>如果不想在每个字段都加 <code>@DateTimeFormat</code> 注解，可以利用 Jackson 进行反序列化相关设置。</p>
<p>既然要设置反序列化，那序列化也少不了。我把全部设置的代码放下面了，有需要复制即可：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 结语</h2>
<p>好了，终于搞完了，我的评价是：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1694501621853-c16a33fa-68e7-4aba-a72e-a9907793b564.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1694499288408-aaec3b0c-9d06-43c1-849b-7af4ec9eb01a.png" type="image/png"/>
    </item>
    <item>
      <title>编写函数的最佳实践</title>
      <link>https://levy.vip/java/recommend-practices-for-writing-good-functions.html</link>
      <guid>https://levy.vip/java/recommend-practices-for-writing-good-functions.html</guid>
      <source url="https://levy.vip/rss.xml">编写函数的最佳实践</source>
      <description>编写函数的最佳实践 前言 编写函数的目的，最根本的目的是提高可维护性，从而提高研发效率。 本文将推荐一些编写函数的最佳实践，以供参数。 减少重复 这是在遵守 Don&amp;apos;t repeat yourself (DRY) 原则。 实践中可以采取一个简单的判断方法：当相同的代码段第二次出现时，就是需要把代码封装成函数的契机。</description>
      <pubDate>Fri, 14 Oct 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 编写函数的最佳实践</h1>
<h2> 前言</h2>
<p>编写函数的目的，最根本的目的是提高可维护性，从而提高研发效率。</p>
<p>本文将推荐一些编写函数的最佳实践，以供参数。</p>
<h3> 减少重复</h3>
<p>这是在遵守 <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank" rel="noopener noreferrer">Don't repeat yourself</a> (DRY) 原则。</p>
<p>实践中可以采取一个简单的判断方法：当相同的代码段第二次出现时，就是需要把代码封装成函数的契机。</p>
<p>然而，有时代码只是相似，不完全相同，不能简单地使用 IDEA 右键 + Refactor + Extract Method 来抽取函数。<br>
此时，为减少重复，需要进行一些思考。</p>
<p>可以把程序的划分成三个部分：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>一般而言，函数的入参都是数据变量，也即 Data Structure。<br>
而 Java 8 以后，lambda 表达式（也即函数）可以作为入参，其代表的是 Logic。<br>
因此，最抽象的函数，是只定义了 Control、把 Logic 及 Data Structure 都作为入参的函数。当遇到类似却不完全相同的代码、想封装函数有遇难时，可以借助上述思路来梳理逻辑。</p>
<h3> 隐藏细节</h3>
<p>隐藏细节，是为了减少使用者的心智负担，方便其调用。</p>
<p>有一个简单的判断标准：如果调用者需要频繁查看函数内部情况，以确定函数的目的或实现细节，那么隐藏细节的意图是失败的。</p>
<h2> 建议</h2>
<p>为了达到前文所述的目的，如以下实践建议。需要指出的是，以下提倡的是建议，并非金科玉律；只适用于一般情况，并非所有情况，特殊情况是可以特殊处理的。</p>
<h3> 优先根据业务命名</h3>
<p>一般而言，函数名最好是根据业务逻辑、结合业务领域来命名，而不是根据程序逻辑来命名。</p>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当然，如果有些方法名是专业名词或是耳熟能详的，那直接使用即可，如：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 一个函数只做一件事</h3>
<p>遵守 <a href="https://en.wikipedia.org/wiki/KISS_principle" target="_blank" rel="noopener noreferrer">Keep it simple stupid</a> (KISS) 原则。</p>
<p>当然，不可能所有函数都达到这个要求——程序入口一般就会做很多事。我们的目标是尽可能地遵守该原则，减少调用者需要频繁查看函数实现的可能。</p>
<p>反例1：做A且做B</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>坏的示例问题出在哪里呢？根据入参的合法性，有可能产生以下情况：</p>
<ol>
<li>参数合法，同时做A与B；只要有一个参数不合法，均不做A与B</li>
<li>哪个合法就做哪个，也即可能出现：
<ol>
<li>只做A</li>
<li>只做B</li>
<li>做A也做B</li>
<li>二者均不做</li>
</ol>
</li>
</ol>
<p>到底是什么情况呢？对此疑问，调用方只有查看函数实现，才能了解，于是破坏了封装的意图。<br>
而且，坏的示例还会存在一个问题：如果调用方只想做A怎么办？我想很少人会把原代码拆分成两个函数，更常见的做法是保持原函数不变，并拷贝原函数的部分逻辑，封装一个只做A的新函数——这就造成了代码的冗余，于是减少重复的目的失败了。</p>
<p>反例2：做A或做B</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>同样的，坏的示例会让人疑惑，搞不清楚函数的意图到底属于以下哪种情况：</p>
<ol>
<li>要么做A，要么做B，一定会做其中一个</li>
<li>哪个合法做哪个，可能会出现：
<ol>
<li>只做A</li>
<li>只做B</li>
<li>做A也做B</li>
<li>二者均不做</li>
</ol>
</li>
</ol>
<p>当然，一些常见的深入人心的 API，我们是可以接受这种“或逻辑”的：</p>
<ul>
<li>saveOrUpdate() // 有 id 就是 update，没有就是 insert</li>
<li>getOrDefault() // 获取值；如果值不存在，就返回默认值</li>
</ul>
<h3> 优先使用纯函数</h3>
<p>纯函数(pure function)，可以借助数学中的函数概念来理解：y = f(x)</p>
<ul>
<li>给定 x，能返回确定的 y</li>
<li>无论函数调用几次、在何处调用，上述结果都不会变化</li>
</ul>
<p>纯函数的好处之一是无副作用（side effect）。也即调用函数后，不会对函数作用域以外的变量造成影响。</p>
<p>反例：一个常见的现象，辅助函数会修改入参，主函数的变量生命周期贯穿整个主函数</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>因为已经省略了其他代码，因此我们不难看出 <code>nodesMap</code>是辅助变量，是为返回结果而服务的，而没有返回值的函数调用很可能修改了该变量。</p>
<p>但实际上，代码逻辑很长，还有其他变量掺杂其中，代码意图并非能够一目了然。假设稍微修改一下，为 <code>getUpstream()</code>添加多一个参数，还能看出函数到底修改了哪个变量吗？</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果 <code>edgesMap</code>是来自主函数的参数呢？</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>情况变得糟糕了，因为按照 <code>getUpstream()</code>会修改入参的“习性”，我们很难有信心认为 <code>edgesMap</code>一定没有被修改。</p>
<p>上述例子是想表明：为了贪图方便，编写一个不需要返回值而直接修改入参的函数，会给后续的维护增加负担。一方面变量状态难以追踪，另一方面这样的函数也不方便测试。</p>
<p>一般而言，优先使用纯函数，会助于对大函数的拆分，从而使得 KISS 原则更容易被遵守。</p>
<p>当然，总有例外情况。当程序逻辑复杂时，或有些函数就是对 setter 语句的调用，此时不需要返回值并且会造成副作用，又该怎么办呢？请看下一条建议。</p>
<h3> 编写不需要返回值的函数</h3>
<p>有如下建议：</p>
<ol>
<li 要修改的变量名="">方法名叫<code>setup</code>+ $</li>
<li>一个方法只修改一个变量</li>
<li>要修改的变量就是函数的第一个参数</li>
<li>lambda（如果有的话） 作为最后的参数</li>
</ol>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 参考资料</h2>
<ul>
<li><a href="https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch03" target="_blank" rel="noopener noreferrer">functional-programming-mostly-adequate-guide/ch03</a></li>
<li><a href="http://aroma.vn/web/wp-content/uploads/2016/11/code-complete-2nd-edition-v413hav.pdf" target="_blank" rel="noopener noreferrer">code-complete-2nd-edition.pdf</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>枚举的推荐实践</title>
      <link>https://levy.vip/java/using-enum-in-java.html</link>
      <guid>https://levy.vip/java/using-enum-in-java.html</guid>
      <source url="https://levy.vip/rss.xml">枚举的推荐实践</source>
      <description>枚举的推荐实践 背景 定义枚举的动机在于，可以作为常量，避免魔法值的出现。并且具有相应的类型，方便检索、与代码提示。 而在使用过程中，一种符合直觉的想法是，期望枚举在具备基本的 key-value 的功能外，还能够承载更多的信息。 本文推荐，不要在枚举中定义数字，直接使用枚举名即可！ Java 极简实现 理想状态下，枚举就应该这样简单！ public enum SEX { MALE, FEMALE; }</description>
      <pubDate>Fri, 14 Oct 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 枚举的推荐实践</h1>
<h2> 背景</h2>
<p>定义枚举的动机在于，可以作为常量，避免魔法值的出现。并且具有相应的类型，方便检索、与代码提示。</p>
<p>而在使用过程中，一种符合直觉的想法是，期望枚举在具备基本的 key-value 的功能外，还能够承载更多的信息。</p>
<p>本文推荐，不要在枚举中定义数字，直接使用枚举名即可！</p>
<h2> Java</h2>
<h3> 极简实现</h3>
<p>理想状态下，枚举就应该这样简单！</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 常见实现</h3>
<p>然而，代码库中常见的实现是，使用 enum 关键字定义枚举类型，并写下枚举名以及相应的绑定值</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>就这点代码，从业务的角度讲，逻辑已经实现了。</p>
<p>但从具体编程语言（Java）的角度讲，工作还没有完成：此时，编译器会提示报错，因为缺少构造函数。</p>
<p>站在调用方的角度，就会发现，我们没有方法拿到枚举里的定义值，也即<code>(1, "this is description for male")</code> ，</p>
<p>因此，还需要编写以下内容：</p>
<ul>
<li>final 的成员变量</li>
<li>在构造函数中为成员变量赋值</li>
<li>定义成员变量相应的 getter 方法</li>
</ul>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>前面说到，我们期望枚举里有 key-value 的功能，则还要再定义 <code>getValueByKey</code> 方法：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>有了上面的方法，我们才可以通过传入 code，返回相应的 desc</p>
<p>如果反过来，我们想通过传入 desc 返回相应的 code 呢？还得再写一个方法！真是烦琐！</p>
<h3> 更好的方式</h3>
<p>前面我们可以看到，当关键逻辑写出来以后，还要写那么多模板代码，简直索然无味。</p>
<p>为什么把简单的事情搞复杂，有没有更好的方式？</p>
<p>需要了解到以下事实，枚举类型提供了通过枚举名获得枚举值的方法：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>也就是说，如果使用 <code>valueOf</code>的方式来定位枚举值，就可以通过 getter 方法来获取自定义的业务值，不需要再写自定义的 getValueByKey 方法。</p>
<h3> 为什么还要定义数字？</h3>
<p>另外，再看之前的枚举定义，为什么要定义 <code>MALE(1, "this is description for male")</code>呢？<br>
在 <code>MALE</code>本身已经有含义的情况下，为何还要再设置一个数字呢？</p>
<p>这个数字导致了不必要的转换：</p>
<ol>
<li>前端传数字 -&gt; 后端转成枚举</li>
<li>后端再把枚举转成数字 -&gt; 存入数据库</li>
</ol>
<p>最蛋疼的就是，select 数据库数据的时候，全是1、2、3，都不知道什么意思。</p>
<p>能不能不要这个数字？ 直接把定义好的枚举名，存入数据库呢？</p>
<p>表面上看，这是因为数据库定义如此——从数据库查出来是数字，因而要根据数字去获取别的业务信息。</p>
<p>然而，我再问了一下，得到的回答是： 上述做法是设计如此。为了节省存储空间及提升查询性能，才在数据库设置的数字，而不是直接存储字符串。</p>
<p>这就引出了另一个问题，需要这样做来提高性能吗？对此，我们接下来要进行数据库层面的讨论。</p>
<h2> 数据库</h2>
<p>数据库本身是支持检举类型的，下文以 MySQL 为例进行说明。</p>
<p>枚举相关的 SQL 语句示例：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>存储方式：转换成数字存储，查询时再转换成字符串<br>
<a href="https://dev.mysql.com/doc/refman/5.7/en/storage-requirements.html#data-types-storage-reqs-numeric" target="_blank" rel="noopener noreferrer">存储空间</a>：1 or 2 bytes (65,535 values maximum)</p>
<p>由上可知，为了节省空间及提高查询性能，在数据库层面使用数字代表枚举进行存储，是不必要的，因为数据库本身已有相应的功能。</p>
<h3> 排序特点</h3>
<p>排序规则如下（如执行order by 时）：</p>
<ul>
<li>NULL 在最前面，'' 次之，接下来是非空的枚举值</li>
<li>定义枚举值时的顺序，就是排序的顺序</li>
</ul>
<p>推荐使用以下技巧：</p>
<ul>
<li>按字母表顺序定义检举值</li>
<li>把检举值转换成字符串再排序 ORDER BY CAST(col AS CHAR) or ORDER BY CONCAT(col)</li>
</ul>
<h3> 添加新值</h3>
<p>使用枚举类型的最大的问题是，后续添加新值时需要执行 alter table。</p>
<p>如果枚举值经常变动且对枚举值的顺序要求（添加的新值不一定在最后面），则不建议使用枚举类型。</p>
<p>否则的话，可以使用枚举类型。</p>
<p>因为 alter table 的机制是：</p>
<ol>
<li>创建临时表 t'</li>
<li>插入数据</li>
<li>删除当前表 t</li>
<li>把 t' 重命名为 t</li>
</ol>
<p>这在表数据量较大时，会导致表被锁较长时间而不可用。</p>
<p>可以使用以下技巧进行更新，记得严格按照顺序执行：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果在 Navicat 或 DBeaver等图形工具上看不出表结构的变化，请刷新数据库。</p>
<h2> 总结</h2>
<p>本文主要目的是想消除枚举中对魔法数字的误用，试图让 Java 代码、数据库数据、以及前端 API 传参都使用可理解、可读性强的枚举值。</p>
<p>在数据库层面，对于枚举类型字段的注意点，本文也做了说明。如果实在不想每次添加新的枚举值都执行 <code>alter table</code>语句，贪图省事，使用 varchar 存储也未尝不可。</p>
<p>总之，本文想强调的是 Java 代码与数据库数据展示内容的一致性，至于数据库的存储格式，是见仁见智的。</p>
<h2> 参考资料</h2>
<ul>
<li><a href="https://www.baeldung.com/java-enum-values" target="_blank" rel="noopener noreferrer">https://www.baeldung.com/java-enum-values</a></li>
<li><a href="https://www.baeldung.com/a-guide-to-java-enums" target="_blank" rel="noopener noreferrer">https://www.baeldung.com/a-guide-to-java-enums</a></li>
<li><a href="https://dev.mysql.com/doc/refman/5.7/en/enum.html" target="_blank" rel="noopener noreferrer">https://dev.mysql.com/doc/refman/5.7/en/enum.html</a></li>
<li><a href="https://www.oreilly.com/library/view/high-performance-mysql/9781449332471/" target="_blank" rel="noopener noreferrer">High Performance MySQL 3rd Edition</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Boolean 还是 boolean？</title>
      <link>https://levy.vip/java/which-one-is-better-Boolean-or-boolean.html</link>
      <guid>https://levy.vip/java/which-one-is-better-Boolean-or-boolean.html</guid>
      <source url="https://levy.vip/rss.xml">Boolean 还是 boolean？</source>
      <description>Boolean 还是 boolean？ 在 Java 中，对于布尔类型的变量、对象属性或方法参数的定义，到底是用包装类型 Boolean 还是基本类型 boolean 呢？ 结论 先说结论：根据《Effective Java》(第三版），始终尽可能地使用基本类型。故应该使用 boolean。 原文如下：</description>
      <pubDate>Thu, 02 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Boolean 还是 boolean？</h1>
<p>在 Java 中，对于布尔类型的变量、对象属性或方法参数的定义，到底是用包装类型 Boolean 还是基本类型 boolean 呢？</p>
<h2> 结论</h2>
<p>先说结论：根据《Effective Java》(第三版），始终尽可能地使用基本类型。故应该使用 boolean。</p>
<p>原文如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654073705555-ed3a3c4c-bb4b-4bfb-955f-00fc0f0356b4.png" alt=""></p>
<blockquote>
<p>红线处翻译：总结就是，当你有得选的时候，请务必使用基本类型，而非包装类型。</p>
</blockquote>
<p>那什么时候使用包装类型呢？原文如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654073877311-28b24470-1fc2-4560-a752-caa1bcff807c.png" alt=""></p>
<blockquote>
<p>红线处翻译：当你没得选、被强制要求时，才使用包装类型。如：使用泛型（使用集合类、调用参数是泛型参数的方法），以及通过反射进行方法调用(使用 invoke 方法）</p>
</blockquote>
<h2> 争议</h2>
<p>阿里的<a href="https://github.com/alibaba/p3c/blob/master/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C(%E9%BB%84%E5%B1%B1%E7%89%88).pdf" target="_blank" rel="noopener noreferrer">《Java开发手册》</a>有提到，类属性强制使用包装类型。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654076977162-f5358442-5426-4bca-936b-fcedcff0d3d3.png" alt=""></p>
<p>但注意，本文讨论的仅仅是布尔类型，不要发散话题。 那现在就来分析一下，布尔类型有没有必要考虑 null 的情况？</p>
<p>我认为是没有的必要的。理由如下：</p>
<ul>
<li>布尔类型就是二进制的，代码两种情况：1或0；真或假。使用包装类型，出现第三种情况 null，不但要注意空指针异常问题，还要兼容 null 的情况——此时到底是真还是假呢?</li>
<li>如果 null 表示的既不是真也不假，而是第三种情况——就不该定义为布尔类型，而应定义为枚举类型，因为一共有三种情况。使用 Boolean 来表示三种情况，是设计上的偷懒。</li>
</ul>
<h2> 实战</h2>
<p>我们来看一下实际代码中，滥用 Boolean 类型导致的问题。</p>
<h3> 简单例子</h3>
<p>有如下 Controller，使用的是 boolean：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>前端传个空值，会得到报错信息：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695173173498-3a5c865a-912b-429c-bb9c-ee8132e6dee5.png" alt=""><br>
但如果使用 Boolean，并不能检查出是非法参数：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695173219438-a3bf6805-fefb-4ee7-b592-c0472f784d79.png" alt=""><br>
这原本是前端传参错误，但却无法即使发现。</p>
<p>你可能会问，为什么不先判断 Boolean 变量是否为 null  呢？因为别人在声明 Boolean 变量时，设置了默认值为 false，谁能想到前端会传个 null？</p>
<p>这就是 Boolean 的问题：“千防万防，家贼难防”！</p>
<h3> 复杂例子</h3>
<p>下面是前端传给 Controller 的参数：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>需要注意的是，有至少两个 Controller 会接受到了这个参数，并且根据业务的不同，它们对参数的处理是不同的：</p>
<ul>
<li>有的会判断如果 withTable2Api 为 null，就设置为 true</li>
<li>有的则完全由前端传值，也即 forkable、withTable2Api 有可能为 null</li>
</ul>
<p>来看一下，Service 里的相应代码：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654083796570-a55a49ad-dcd3-4e5c-a35d-c3455d786715.png" alt=""><br>
这里有什么问题呢？可以看到，第二个 if 使用 <code>!Boolean._TRUE_</code>去判断很别扭，然而，却不能改写成：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这就是布尔值使用 Boolean 类型在实战中最大的问题——你不能任意地进行真或假的判断，而必须兼容上下文中隐式对 null 赋予的含义。</p>
<p>而多人协作过程中，外部调用是很难控制的，因此，此时使用 Boolean，只增加了无谓的编码负担。</p>
<h2> 附</h2>
<p><a href="https://www.yuque.com/attachments/yuque/0/2022/pdf/160590/1654077188552-25d2e37f-f34c-46db-817e-569163265605.pdf?_lake_card=%7B%22src%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2022%2Fpdf%2F160590%2F1654077188552-25d2e37f-f34c-46db-817e-569163265605.pdf%22%2C%22name%22%3A%22Joshua%20Bloch%20-%20Effective%20Java%20(3rd)%20-%202018.pdf%22%2C%22size%22%3A2294786%2C%22ext%22%3A%22pdf%22%2C%22source%22%3A%22%22%2C%22status%22%3A%22done%22%2C%22download%22%3Atrue%2C%22type%22%3A%22application%2Fpdf%22%2C%22mode%22%3A%22title%22%2C%22taskId%22%3A%22u4da28c90-dfee-481c-b1b9-7ab6c120cdd%22%2C%22taskType%22%3A%22upload%22%2C%22__spacing%22%3A%22both%22%2C%22id%22%3A%22ud6462faf%22%2C%22margin%22%3A%7B%22top%22%3Atrue%2C%22bottom%22%3Atrue%7D%2C%22card%22%3A%22file%22%7D" target="_blank" rel="noopener noreferrer">Joshua Bloch - Effective Java (3rd) - 2018.pdf</a></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654073705555-ed3a3c4c-bb4b-4bfb-955f-00fc0f0356b4.png" type="image/png"/>
    </item>
    <item>
      <title>forEach 还是 map？</title>
      <link>https://levy.vip/java/which-one-is-better-forEach-or-map.html</link>
      <guid>https://levy.vip/java/which-one-is-better-forEach-or-map.html</guid>
      <source url="https://levy.vip/rss.xml">forEach 还是 map？</source>
      <description>forEach 还是 map？ 背景 遍历一个集合，在里面执行某种操作后，再依次返回每一个元素，常见的实现方式有： List&amp;lt;Type&amp;gt; result = new ArrayList&amp;lt;&amp;gt;(); list.forEach(src -&amp;gt; { Type target = BeanUtils.copyProperties(src, target); //省略代码 result.add(target); });</description>
      <pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> forEach 还是 map？</h1>
<h2> 背景</h2>
<p>遍历一个集合，在里面执行某种操作后，再依次返回每一个元素，常见的实现方式有：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>两种方式看起来没多大差别啊，到底用哪种呢？</p>
<h2> 结论</h2>
<p>先说结论：根据《Effective Java》(第三版），forEach 只用于消费数据的场景，并不应该用于计算、累加，故上述代码应该使用 map。</p>
<p>原文如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654151903594-61668916-08c9-4349-9429-024742b14fac.png" alt=""></p>
<blockquote>
<p>红字处翻译：forEach 仅适用于输出 stream 里的计算结果，并不适合执行计算。</p>
</blockquote>
<h2> 解析</h2>
<p>为更好地理解上述结论，需要先理解以下内涵：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654153524133-3fcb016b-8dae-4482-807b-952b5dd0ca6e.png" alt=""><br>
Stream 的引入，不仅带来新的语法，也带来了函数式编程的思维。</p>
<p>这里最重要的一点就是：编写纯函数（pure function），不造成副作用（side-effect）。</p>
<p>纯函数可以用数学中的函数映射来理解：y = f(x)</p>
<ul>
<li>给定 x，能唯一确定 y</li>
<li>无论函数调用几次、在何处调用，上述结果都不会变化</li>
</ul>
<p>无副作用意思是：调用函数后，不会对函数作用域以外的变量造成影响。而纯函数，一定是无副作用的。</p>
<p>前文使用 forEach 的代码，其实是造成了副作用的：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>很简单的一个识别方法：再调用一次 forEach，result 的结果还是期望的结果吗？显然不是。</p>
<p>但如果 map 方法呢？再调用一次，结果不变！</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>类似的，修改函数入参，也不是纯函数：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>上面的 <code>collectIds</code>函数是令人讨厌的——写代码的人懒得写函数返回时，直接修改函数入参，给后面维护的人留下隐患。</p>
<p>当然，如果一定要用纯函数来看待问题，未免过于理想化，因为有时要执行这样的代码：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>虽然上述代码并没影响到函数作用域以外的代码变量，但 myService 会把数据持久化，站在整个应用的角度讲，仍然造成了副作用。</p>
<p>但上述代码可以接受的。因此，建议记住 forEach 只用于消费数据，不用于计算及返回，就不容易混淆。</p>
<h2> 实战</h2>
<p>当然，上述的讨论还是比较偏理论的，我们来看一下实际项目中，滥用 forEach 可能导致可读性较差的问题。</p>
<p>代码一开始，是简单清晰的：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>然而，业务会变化，逻辑会复杂，代码也要修改。而上述在 forEach 中修改变量的行为，罪恶的根源在于，它在向后来修改代码的人发出邀请：新增的逻辑，写在这个 forEach 里面就好了！</p>
<p>当仅在 forEach 添加代码就能完成任务的时候，很难有人能抵抗这种诱惑，于是就会变成：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>写代码一时爽，维护火葬场。上面的代码，将是维护的噩梦！</p>
<p>并且，如果维护者只想知道函数的整体逻辑，由于变量穿插、隐藏在 forEach 内部，维护者不得不在各种 if-else 里面追踪变量，很容易就陷入不必要的细节中。</p>
<p>如果换个方式来写呢？</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>效果有大大的不同！</p>
<p>现在想追查哪个变量，简单轻松好多啦！</p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1654151903594-61668916-08c9-4349-9429-024742b14fac.png" type="image/png"/>
    </item>
    <item>
      <title>Jackson 经典异常 UnrecognizedPropertyException</title>
      <link>https://levy.vip/java/why-i-prefer-fastjson-instead-of-jackson.html</link>
      <guid>https://levy.vip/java/why-i-prefer-fastjson-instead-of-jackson.html</guid>
      <source url="https://levy.vip/rss.xml">Jackson 经典异常 UnrecognizedPropertyException</source>
      <description>Jackson 经典异常 UnrecognizedPropertyException 原因是 json 包含的字段，多于 Java 实体类定义的字段。 解决方法很简单： new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 或者为相关实体添加注解： @JsonIgnoreProperties(ignoreUnknown = true) public class ObjectParseFromJsonString { }</description>
      <pubDate>Mon, 21 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Jackson 经典异常 UnrecognizedPropertyException</h1>
<p>原因是 json 包含的字段，多于 Java 实体类定义的字段。</p>
<p>解决方法很简单：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>或者为相关实体添加注解：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><!-- more -->
<p>可是，如果用 fastjson，根本不会有这种错误。使用起来也简单，<a href="https://github.com/alibaba/fastjson/wiki/Samples-DataBind" target="_blank" rel="noopener noreferrer">文档在这里</a>。</p>
<p>所以，为什么不用　fastjson　呢？</p>
]]></content:encoded>
    </item>
    <item>
      <title>升个jar版本，怎么这么难？</title>
      <link>https://levy.vip/java/why-is-it-so-hard-to-upgrade-dependencies.html</link>
      <guid>https://levy.vip/java/why-is-it-so-hard-to-upgrade-dependencies.html</guid>
      <source url="https://levy.vip/rss.xml">升个jar版本，怎么这么难？</source>
      <description>升个jar版本，怎么这么难？ 前言 无论是 C/S 还是 B/S 模式下的应用通过是 API 来交互的。而 API 的消费者与提供者，可以看成一枚硬币的正反面。 通常来说，对 Web 应用而言，前端是消费者，后端是提供者。 今天就站在服务提供方的视角来看下，升级依赖版本，遇到的难点有哪些？</description>
      <pubDate>Sat, 26 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 升个jar版本，怎么这么难？</h1>
<h2> 前言</h2>
<p>无论是 C/S 还是 B/S 模式下的应用通过是 API 来交互的。而 API 的消费者与提供者，可以看成一枚硬币的正反面。</p>
<p>通常来说，对 Web 应用而言，前端是消费者，后端是提供者。</p>
<p>今天就站在服务提供方的视角来看下，升级依赖版本，遇到的难点有哪些？</p>
<!-- more -->
<h2> 接口调用示意图</h2>
<figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1693060849195-8f65efab-8c15-4731-bf8e-10645785f0f5.jpeg" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 已知信息</h2>
<ol>
<li>common.jar 的版本是 1.0.0-SNAPSHOT，实际上 common.jar 的最新稳定版已经是 5.x 了。比较了下版本内容，发现二者是不兼容的。</li>
<li>公司有多个业务系统依赖 infra-service，但它们隶属于别的团队</li>
</ol>
<h2> 问题</h2>
<p>infra-service 的某个接口，现在要修改，多返回一个字段，但因为之前的代码健壮性不够，新接口　common.jar 处理会报错。</p>
<p>于是，想通过让使用方升级 jar 来解决，此方案可行吗？</p>
<p>答案可能出乎意料：不行！</p>
<p>因为：jar 升级版本的动作不在自己的掌控范围内。</p>
<p>说起来很简单：叫他们升一下就行。<br>
但问题是：</p>
<ol>
<li>他们是谁？</li>
<li>你说升就升？</li>
</ol>
<p>问题本身在技术层面很简单，但在现实的执行层面，有着超出技术范畴的难点。这便是服务提供者，需要面对的。而这是作为服务的消费者，容易忽视的一点。</p>
<h2> 复盘</h2>
<p>那么，在技术层面，是否能做得更好，避免重蹈覆辙呢？其实是可以的。</p>
<p>有两种思路：</p>
<ol>
<li>只对外发布稳定版，要么强制自己的接口一直向后兼容；要么一旦有接口不兼容，在保留旧接口的情况下，发布新接口、新 jar。</li>
<li>利用好 SNAPSHOT 版本 jar 会不断覆盖的特性，同时对外发布 1.0.0-SNAPSHOT 版本，以及每次迭代的稳定版。也即，通过使得 1.0.0-SNAPSHOT 永远与最新的稳定版本相同，让使用者及时升级。</li>
</ol>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1693060849195-8f65efab-8c15-4731-bf8e-10645785f0f5.jpeg" type="image/jpeg"/>
    </item>
    <item>
      <title>使用Ragas评估LLM应用</title>
      <link>https://levy.vip/llm/evaluate-llm-app-with-ragas.html</link>
      <guid>https://levy.vip/llm/evaluate-llm-app-with-ragas.html</guid>
      <source url="https://levy.vip/rss.xml">使用Ragas评估LLM应用</source>
      <description>使用Ragas评估LLM应用 说明 对于已知问题有正确答案的场景，适合使用 ragas 的 faithfulness 指标对 GenAI 应用响应结果进行评估，方便进行回归测试。 注意：本文提到的方法，只适用于对已知问题的评估。对于线上运行时，用户提的随机的、不在测试集范围内的问题，不适合用此方法评估。 安装 pip install ragas</description>
      <pubDate>Wed, 03 Apr 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用Ragas评估LLM应用</h1>
<h2> 说明</h2>
<p>对于已知问题有正确答案的场景，适合使用 ragas 的 faithfulness 指标对 GenAI 应用响应结果进行评估，方便进行回归测试。</p>
<p>注意：本文提到的方法，只适用于对已知问题的评估。对于线上运行时，用户提的随机的、不在测试集范围内的问题，不适合用此方法评估。</p>
<h2> 安装</h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 数据说明</h2>
<p>对以下数据进行评估。</p>
<p>事实：Einstein was born in 1879 in Germany.<br>
提问：</p>
<ol>
<li>When did Einstein born?</li>
<li>Where did Einstein born?</li>
</ol>
<p>正确答案：</p>
<ol>
<li>Einstein was born in 1879.</li>
<li>Einstein was born in Germany.</li>
</ol>
<h2> 正确性❌</h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1711614954321-45e4ad75-2c87-42a9-9021-6455f01ec484.png" alt=""><br>
这是不能令人满意的——正确的回答，得到的指标分数却不足0.8。<br>
这是因为，正确性的评估，还依赖了相似度。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1711614692649-234f7b6a-16c5-4c09-95e7-db2877663e4c.png" alt=""></p>
<h2> 忠实度✔</h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1711614916136-dfc65b51-b0ec-41a6-8bae-d0907806fb49.png" alt=""><br>
符合预期，满足要求！</p>
<h2> 实战演示</h2>
<h3> 准备好样例问题</h3>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 准备好正确答案</h3>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 编写回答函数</h3>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 进行答案评估</h3>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>效果如下：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1712136338962-974f31a0-1b27-404e-92de-b26dc6de8adb.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1711614954321-45e4ad75-2c87-42a9-9021-6455f01ec484.png" type="image/png"/>
    </item>
    <item>
      <title>大语言模型赋能备案审查</title>
      <link>https://levy.vip/llm/llm-with-recordation-review-of-regulations.html</link>
      <guid>https://levy.vip/llm/llm-with-recordation-review-of-regulations.html</guid>
      <source url="https://levy.vip/rss.xml">大语言模型赋能备案审查</source>
      <description>大语言模型赋能备案审查 业务背景 备案审查是指规范性文件在制定颁布后，按法定期限报同级或上一级人大常委会备案，由接受备案的人大常委会在法定期限内依照法定标准和程序对其进行监督审查的活动。 在这个过程中，最重要的就是确保下位法不会与上位法相抵触。 因规范性文件众多，而具备审查资格的专家又极少，故工作进程较为缓慢，现期望借助AI的能力，提高效率。 核心流程</description>
      <pubDate>Wed, 29 May 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 大语言模型赋能备案审查</h1>
<h2> 业务背景</h2>
<p>备案审查是指规范性文件在制定颁布后，按法定期限报同级或上一级人大常委会备案，由接受备案的人大常委会在法定期限内依照法定标准和程序对其进行监督审查的活动。<br>
在这个过程中，最重要的就是确保下位法不会与上位法相抵触。<br>
因规范性文件众多，而具备审查资格的专家又极少，故工作进程较为缓慢，现期望借助AI的能力，提高效率。</p>
<h2> 核心流程</h2>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/1716934384864-e8f15645-a3a7-4986-8610-f964433eb8f7.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h2> 项目难点</h2>
<ol>
<li>海量文件，必须自动化分片，不可能人工处理，也即手工插入分隔符是不可行的</li>
</ol>
<p>解决方案：写代码处理</p>
<ol start="2">
<li>通过下位法法条，能找到对应的上位法法条</li>
</ol>
<p>解决方案：在分片质量有所保证的提前下，在常规语义检索的基础上，通过结构化数据筛选，提高匹配率</p>
<ol start="3">
<li>判断下位法是否与上位法相抵触</li>
</ol>
<p>解决方案：参考经典案例，学习其判断逻辑；指示AI，无法判断时不要乱下结论。</p>
<h2> 经验总结</h2>
<p>数据很重要，一定要了解业务数据。</p>
<p>数据要分类：</p>
<ol>
<li>参考的数据要与验证的数据区分开来。一定要在前期就找客户要参考案例，并问清楚如何验证。问清楚后，要么让客户给验证数据，要么自己造验证数据，千万不能连怎么验证都不知道就动手。</li>
<li>基于场景分类，如下位法对上位法范围扩大、范围缩小，它们就要区分开来。</li>
</ol>
<h2> 参考资料</h2>
<p><a href="https://baike.baidu.com/item/%E4%B8%8A%E4%BD%8D%E6%B3%95" target="_blank" rel="noopener noreferrer">什么是上位法</a><br>
<a href="https://wap.zuel.edu.cn/2023/0615/c1236a337820/pagem.htm" target="_blank" rel="noopener noreferrer">论大语言模型在规范性文件备案审查中的应用</a><br>
<a href="https://www.gdpc.gov.cn/gdrdw/rdzt/bascalxb/" target="_blank" rel="noopener noreferrer">备案审查案例选编</a><br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/1716934571936-866c2e58-59e4-4c4a-aae6-4c2b54624d7d.png#averageHue=%23f8f3f1&amp;clientId=ue4d82166-db4f-4&amp;from=paste&amp;height=385&amp;id=u86ef6742&amp;originHeight=578&amp;originWidth=1397&amp;originalType=binary&amp;ratio=1.5&amp;rotation=0&amp;showTitle=false&amp;size=168640&amp;status=done&amp;style=none&amp;taskId=ud877cee2-9be7-4ce6-a968-98c30e15f45&amp;title=&amp;width=931.3333333333334" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/1716934384864-e8f15645-a3a7-4986-8610-f964433eb8f7.png" type="image/png"/>
    </item>
    <item>
      <title>数据备份案例：mysqldump实战</title>
      <link>https://levy.vip/mysql/mysql-backup-case-study-mysqldump-in-action.html</link>
      <guid>https://levy.vip/mysql/mysql-backup-case-study-mysqldump-in-action.html</guid>
      <source url="https://levy.vip/rss.xml">数据备份案例：mysqldump实战</source>
      <description>数据备份案例：mysqldump实战 背景 前面有讲数据迁移的案例(mysql-a -&amp;gt; mysql-b)，其实在迁移前还少不了备份。 并且，因为不想停机迁移，因此还要新起一个数据库实例，记为 mysql-b&amp;apos;，复制 mysql-b 的相关数据。这样就能在 mysql-b&amp;apos; 里验证迁移SQL的正确性，以确保 mysql-b 能不宕机完成数据迁移。 在这种情况下，就需要用到我们今天的主角，数据备份工具 mysqldump。</description>
      <pubDate>Thu, 17 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 数据备份案例：mysqldump实战</h1>
<h2> 背景</h2>
<p>前面有讲<a href="/mysql/mysql-data-migration-case-study-add-auto-increment.html" target="blank">数据迁移的案例</a>(mysql-a -&gt; mysql-b)，其实在迁移前还少不了备份。</p>
<p>并且，因为不想停机迁移，因此还要新起一个数据库实例，记为 mysql-b'，复制 mysql-b 的相关数据。这样就能在 mysql-b' 里验证迁移SQL的正确性，以确保 mysql-b 能不宕机完成数据迁移。</p>
<p>在这种情况下，就需要用到我们今天的主角，数据备份工具 mysqldump。</p>
<!-- more -->
<h2> 架构</h2>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692280572283-08eb507c-00b6-446b-bd37-cb5ecbe59531.jpeg" alt=""><br>
注意到：</p>
<ol>
<li>从 mysql-b -&gt; mysql-b'，就要用到工具 mysqldump</li>
<li>关于 sql 的编写在另一文中已有提及，就不重复了</li>
<li>只能通过跳板机在终端连接数据库实例，因此无法使用图例界面操作。</li>
</ol>
<h2> 安装</h2>
<p>首先要在跳板机安装 mysqldump。</p>
<p>如果够幸运，可以用包管理工具安装：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果配置有问题安装不了，可以直接下载 mysql 二进制包：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果还不行，那就本地下载，然后把 mysqldump 压缩通过 scp 上传到跳板机。</p>
<p>有人会想，这么麻烦，为何不直接复制 mysql-b 的数据文件？这是因为：</p>
<ol>
<li>没有进入 mysql-b 宿主机的权限</li>
<li>复制磁盘文件更麻烦，后面会讲</li>
</ol>
<h2> 导出</h2>
<p>导出命令很简单的，类似 mysql 客户端连接，指定用户名、表名、导出文件，再根据提示输入密码即可。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692281767457-f40d7db4-c2d9-4589-ac46-cbea320266da.png" alt=""><br>
然而命令行的提示，暗示事情令有玄机：</p>
<ol>
<li>首先，这样简单的命令，只能导出表数据，其他数据如触发器则并没导出</li>
<li>其次，由于目标数据库开启了 GTID 模式(gtid_mode=ON)，导出的文件会有相关的设置</li>
</ol>
<h3> --set-gtid-purged=OFF</h3>
<p><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692281766948-704c4a90-c575-4340-b596-281c74de5c44.png" alt=""><br>
打开 dump.sql 的前 30 行，可以看到 GTIDs 的相关内容。</p>
<p>Here's a quick overview of how GTIDs(Global Transaction Identifiers) work in MySQL：</p>
<ul>
<li>GTIDs uniquely identify every transaction.</li>
<li>Allow replicas to track replication positions.</li>
<li>Enable failover and repointing replication.</li>
</ul>
<p>因为 mysql-b 是主从架构的，开启 GTIDs 很正常，但我们要用来测试的 mysql-b' 是单实例的，并不需要它。<br>
如果未正确设置，导入的时候是会报错的。</p>
<p>因此，在这种情况下，导出语句可以添加以下参数：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>也即命令变成：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>当然，也可以修改 dump.sql，把 GTIDs 相关的语句删除掉。</p>
<h3> --ignore-table</h3>
<p>导出的文件可能会很大，而如果我们要把文件在不同的机器之间传输，这就会让过程变得很慢。此时，我们希望导出的文件越小越好。</p>
<p>我们利用以下 SQL来确认下，目标库中到底是哪些表占据了较大的存储空间：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>接着我们根据业务，分析哪些表的数据是必要的，哪些表的数据可以不用导出。添加以下参数把不需要导出的表忽略掉：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>示例命令：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>注意：该参数一次只能忽略一张表，故忽略多张表需要声明多次。</p>
<h2> 导入</h2>
<p>导入命令类似导出，只不过此时使用的是 mysql　客户端，并且重定向符号不同。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 为什么不?</h2>
<p>现在来回答为什么不直接复制MySQL的磁盘文件。</p>
<p>根据前面我们知道，也许我们的需求是部分备份，而不是全量备份，则直接拷贝磁盘文件，在数据量大的情况下，只会造成传输负担，反而“欲速则不达”。</p>
<p>另外，复制磁盘文件，为保证数据一致性，要求MySQL必须停止运行。主要有以下原因：</p>
<ol>
<li><strong>Ative Transactions:</strong> Copying database files directly may lead to data inconsistency if there are active transactions or changes happening in the database while the files are being copied.</li>
<li><strong>Flush and Sync:</strong> The MySQL server may buffer data in memory and write it to disk periodically. When you copy files directly, you may capture data that is in memory and not yet written to disk.</li>
<li><strong>File Locks:</strong> Some storage engines, such as MyISAM, may use file-level locks, which can prevent you from copying files while the server is running. InnoDB, on the other hand, uses a different mechanism (tablespace files), but copying InnoDB files still carries the risk of data inconsistency.</li>
</ol>
<p>因此，纵然有直接复制MySQL磁盘文件的奇技淫巧，还是不建议使用，我就也不向大家展示了。</p>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692280572283-08eb507c-00b6-446b-bd37-cb5ecbe59531.jpeg" type="image/jpeg"/>
    </item>
    <item>
      <title>数据迁移案例：表AUTO_INCREMENT加10w</title>
      <link>https://levy.vip/mysql/mysql-data-migration-case-study-add-auto-increment.html</link>
      <guid>https://levy.vip/mysql/mysql-data-migration-case-study-add-auto-increment.html</guid>
      <source url="https://levy.vip/rss.xml">数据迁移案例：表AUTO_INCREMENT加10w</source>
      <description>数据迁移案例：表AUTO_INCREMENT加10w 背景 项目要做数据迁移，要把 mysql-a 的数据，迁移至 mysql-b，同时 mysql-b 的数据不能丢失。 问题分析： 两个 mysql 实例的表的主键都是自增的，若直接合并，必然造成主键冲突。 可以修改某一方的主键后再迁移，但要注意后续不会因主键增长而发生冲突。 迁移思路: 由于 mysql-b 的数据更重要、且数据量更大，故决定修改 mysql-a 的数据的主键，方案是增加 10w（mysql-b 的单表数据不超过 10w条） mysql-b 的相应的表 AUTO_INCREMENT 加 10w 记得动手前先确保数据已备份 注意： mysql-b 的数据量比 mysql-a 的大，所以 mysql-b 也直接设置 AUTO_INCREMENT 加10w。 但如果 mysql-a的数量比较大，那是不可行的，此时 mysql-b 需要先 select 出每张表的最大id，作为需要增加的 AUTO_INCREMENT。</description>
      <pubDate>Wed, 16 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 数据迁移案例：表AUTO_INCREMENT加10w</h1>
<h2> 背景</h2>
<p>项目要做数据迁移，要把 mysql-a 的数据，迁移至 mysql-b，同时 mysql-b 的数据不能丢失。</p>
<p>问题分析：</p>
<ol>
<li>两个 mysql 实例的表的主键都是自增的，若直接合并，必然造成主键冲突。</li>
<li>可以修改某一方的主键后再迁移，但要注意后续不会因主键增长而发生冲突。</li>
</ol>
<p>迁移思路:</p>
<ol>
<li>由于 mysql-b 的数据更重要、且数据量更大，故决定修改 mysql-a 的数据的主键，方案是增加 10w（mysql-b 的单表数据不超过 10w条）</li>
<li>mysql-b 的相应的表 AUTO_INCREMENT 加 10w</li>
<li>记得动手前先确保数据已备份</li>
</ol>
<p>注意：</p>
<ul>
<li>mysql-b 的数据量比 mysql-a 的大，所以 mysql-b 也直接设置 AUTO_INCREMENT 加10w。</li>
<li>但如果 mysql-a的数量比较大，那是不可行的，此时 mysql-b 需要先 select 出每张表的最大id，作为需要增加的 AUTO_INCREMENT。</li>
</ul>
<!-- more -->
<h2> 备份</h2>
<p>迁移前一定要做好备份。备份的技巧在<a href="/mysql/mysql-backup-case-study-mysqldump-in-action.html" target="blank">另一篇文章</a>里有讲，就不在此赘述了。</p>
<h2> SQL编写</h2>
<p>mysql-a 可以先 insert：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>再导出新增的数据：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>也可以先备份 mysql-a，然后直接 update：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>再导出。</p>
<p>对于 mysql-b，需要编写的SQL很简单：</p>
<ol>
<li>先把当前表的主键自增值找出来</li>
<li>再加 10w</li>
</ol>
<p>理想中SQL如下：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>然而，报错如下：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>原因是： The <strong>AUTO_INCREMENT</strong> value must be a constant or a specific integer value in the <strong>ALTER TABLE</strong> statement.</p>
<p>那怎么办呢？存储过程就派上用场了。思路是使用存储过程来拼接 alter table 语句，绕过 MySQL 的限制。</p>
<h2> 存储过程（可复用</h2>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>上述代码可复用，直接复制粘贴即可，有需要的请自取。</p>
]]></content:encoded>
    </item>
    <item>
      <title>MySQL 命令行执行SQL的细节</title>
      <link>https://levy.vip/mysql/mysql-details-you-should-know-when-execute-sql-in-command-line.html</link>
      <guid>https://levy.vip/mysql/mysql-details-you-should-know-when-execute-sql-in-command-line.html</guid>
      <source url="https://levy.vip/rss.xml">MySQL 命令行执行SQL的细节</source>
      <description>MySQL 命令行执行SQL的细节 背景 经过调试与验证，我们可以确信自己编写的SQL是正确的，是时候到目标库执行SQL了！ 但要小心，在正式环境中执行 SQL，也许会有意想不到的坑！ 环境说明 先说明下我们的环境信息。 我们只能通过跳板机的终端连接 mysql、执行SQL，没有DBeaver、Navicat等工具可用。</description>
      <pubDate>Sat, 19 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> MySQL 命令行执行SQL的细节</h1>
<h2> 背景</h2>
<p>经过调试与验证，我们可以确信自己编写的SQL是正确的，是时候到目标库执行SQL了！</p>
<p>但要小心，在正式环境中执行 SQL，也许会有意想不到的坑！</p>
<h2> 环境说明</h2>
<p>先说明下我们的环境信息。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692321838318-b353f14d-13d0-432e-a05b-9fbcd3c1dd1f.jpeg" alt=""><br>
我们只能通过跳板机的终端连接 mysql、执行SQL，没有DBeaver、Navicat等工具可用。</p>
<p>则我们执行SQL语句的方式有两种：</p>
<ol>
<li>执行导出的SQL语句文件</li>
<li>复制粘贴SQL语句执行</li>
</ol>
<p>当然，在执行前，我们要确保已进行了数据备份。</p>
<h2> 执行SQL文件</h2>
<p>执行SQL文件是最简单的，一般实践也是在命令行批量执行SQL文件。</p>
<p>相关的命令与恢复备份的命令一致：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>这是推荐的方式，因为执行语句一旦出错，就会停下，并告知是第几行的语句出错。</p>
<p>但出于某些原因，你可能不想把所有SQL语句都合并到一个 script.sql 文件中。<br>
另外，上传文件到跳板机，可能也比较麻烦，于是，你想采用第二种方式。</p>
<h2> 复制粘贴执行</h2>
<p>通过 mysql 客户端直接上 MySQL 后，在命令上执行 SQL 语句会有一个问题：错误的语句不会中断后续的执行。</p>
<p>更可怕的是在命令行里，很可能你SQL语句包括的中文字符串会被过滤掉，变成空字符串。如：</p>
<ul>
<li>'创建人' -&gt; ''</li>
<li>'中英en混杂' -&gt; 'en'</li>
</ul>
<p>这真是血的教训😭。</p>
<p>我们先来看下错误是否中断的实验。</p>
<p>新建一个只有两行的SQL文件，其中第一行语句是错误的。</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><figure><img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692263770642-5487130c-27ed-4e08-9e89-24755194f7ed.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>使用导入命令：</p>
<div class="language-sql line-numbers-mode" data-ext="sql"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>提示第一行有错误，第二行未执行。这是符合期望的。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692263858957-62a01998-d47a-4fa9-b2a9-b83c5a3be764.png" alt=""></p>
<p>但如果连接 mysql 后，在交互式命令行里执行 source 命令：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692263665731-d9bdde27-6522-4cd3-9c82-4f5c7a0ead79.png" alt=""><br>
错误出现并不会中断SQL的执行。<br>
复制语句，粘贴到命令行，表现也是如此：错误只提示，不中断执行。</p>
<p>那么，能否在交互式命令行里执行SQL语句，一旦错误就中断呢？</p>
<p>我们在MySQL官方论坛里找到了相关的<a href="https://bugs.mysql.com/bug.php?id=35634" target="_blank" rel="noopener noreferrer">帖子</a>：<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692262908887-64d86cd8-c2e9-4b50-b341-75a28a509d94.png" alt=""></p>
<p>最终得到的答案是，使用：\e。<br>
进行类vi界面，在这里粘贴 SQL（这里就不会有中文被过滤的问题）<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692263386275-ba3e4365-f45e-435e-a269-05c5d92dfa16.png" alt=""><br>
保存退出后，输入 <code>;</code>则执行 SQL 语句，Ctrl + C　则不执行。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692263595582-2f5a733e-ea75-47b1-b128-f76ecf7a1cda.png" alt=""><br>
可以看到，这种方式执行 SQL 语句，也是可以遇到错误就中断的。</p>
<h2> 如果要删除错误的数据怎么办？</h2>
<p>尽管经过测试，但也不敢说语句的执行能百分之分成功，因此，这里给个温馨提醒。</p>
<p>如果插入、更新了错误的数据，确实要执行 DELETE 语句，那么请做好以下 checklist:</p>
<ol>
<li>确保已备份数据</li>
<li>删除前先查询，也即先写 select from，确认一下是目标数据，再改写成 select 改写成 delete</li>
<li>一定要写 where 语句，并且精确到主键，最好只写类似这种语句 where id = 1 或者 where id in (1,2)</li>
</ol>
]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1692321838318-b353f14d-13d0-432e-a05b-9fbcd3c1dd1f.jpeg" type="image/jpeg"/>
    </item>
    <item>
      <title>给LLM应用添加日志</title>
      <link>https://levy.vip/python/add-logging-for-llm-app.html</link>
      <guid>https://levy.vip/python/add-logging-for-llm-app.html</guid>
      <source url="https://levy.vip/rss.xml">给LLM应用添加日志</source>
      <description>给LLM应用添加日志 logging替代print 目前公司的LLM应用开发使用的是 Python 技术栈，观察源码，发现没有多少日志，纵使有，也是用的 print。 print 的作用，就相当于 Java 的 System.out.print，相当于 Node.js 的 console.log，一般只适合在本地调试，不适合作为日志输出的。</description>
      <pubDate>Mon, 04 Dec 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 给LLM应用添加日志</h1>
<h2> logging替代print</h2>
<p>目前公司的LLM应用开发使用的是 Python 技术栈，观察源码，发现没有多少日志，纵使有，也是用的 print。</p>
<p>print 的作用，就相当于 Java 的 System.out.print，相当于 Node.js 的 console.log，一般只适合在本地调试，不适合作为日志输出的。</p>
<!-- more -->
<p>打包成 Docker 镜像时，Python 应用很可能看不到 <a href="https://stackoverflow.com/questions/29663459/why-doesnt-python-app-print-anything-when-run-in-a-detached-docker-container" target="_blank" rel="noopener noreferrer">print 输出</a>。使用 pytest 执行测试用例时，<a href="https://stackoverflow.com/questions/24617397/how-do-i-print-to-console-in-pytest" target="_blank" rel="noopener noreferrer">默认也是看不到 print</a> 输出的。再加上 print 太简陋了，输出没有时间，没有级别分类，建议还是弃用，改用专门的日志模块。</p>
<p>使用 Python 内置的日志模块 logging，直接 import 即可使用：</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果要在 pytest 中显示日志，还要在项目根目录添加 pytest.ini 文件，补充如下内容：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>logging 模块完整的用法，可以<a href="https://betterstack.com/community/guides/logging/how-to-start-logging-with-python/" target="_blank" rel="noopener noreferrer">点击查看文章</a>。</p>
<h2> 何时打印日志</h2>
<p>为避免排查线上问题时，被迫吐槽：“怎么一点日志都没有！”建议平时养成打日志的习惯，方便应用的迭代与维护。</p>
<p>下面给出一些通用的打印日志的实践建议，与语言无关，可按需采纳。</p>
<ol>
<li>外部调用</li>
<li>异常捕获</li>
<li>提前返回</li>
<li>复杂或特殊的if-else</li>
</ol>
<h3> 外部调用</h3>
<p>调用另一个应用的API，与中间件（如 redis, rocketmq）交互，都属于外部调用，最好调用前后都打印日志（当然，如果返回的数据量太大，酌情可以考虑省略打印部分返回信息）</p>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 异常捕获</h3>
<p>异常捕获后，一定要打印日志。实在想偷懒，直接打印堆栈信息都能授受。最忌讳的是，捕获了异常，然后什么都不做，直接把异常信息给“吃了”，这绝对是排查问题的恶手。</p>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 提前返回</h3>
<p>如果函数有提前 return 的逻辑，最好返回前也打印日志，不然排查问题时，发现请求进来了，却什么日志也没有，容易让人一头雾水。</p>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 复杂或特殊的if-else</h3>
<p>如何定义复杂，又如何定义特殊，这就见仁见智，需要个人在实践中去总结理解了。</p>
<p>一个常见的场景是，某段逻辑因为业务变化要加 if-else 进行特殊处理，你得在这个 if-else 前加上注释，解释其原因。那么这段逻辑就可以添加日志，日志内容就是你的注释内容，也即把注释改写成日志即可。</p>
<p>示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
    </item>
    <item>
      <title>Python 导出 MySQL 库表信息到 Excel</title>
      <link>https://levy.vip/python/export-mysql-table-into-excel.html</link>
      <guid>https://levy.vip/python/export-mysql-table-into-excel.html</guid>
      <source url="https://levy.vip/rss.xml">Python 导出 MySQL 库表信息到 Excel</source>
      <description>Python 导出 MySQL 库表信息到 Excel 需求 查询 MySQL 某个库的全部表的元信息，输出成 Excel，每一张表一个 sheet。</description>
      <pubDate>Sun, 05 Mar 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> Python 导出 MySQL 库表信息到 Excel</h1>
<h2> 需求</h2>
<p>查询 MySQL 某个库的全部表的元信息，输出成 Excel，每一张表一个 sheet。</p>
<!-- more -->
<h2> 代码</h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 其他细节</h2>
<p>上述代码由于库的原因，只能生成新文件或覆盖文件，不能修改原有文件。<br>
执行代码时必须关闭生成的文件，否则报错。<br>
sheet的名字不能超过 31 个字符。<br>
<a href="https://www.rapidtables.com/convert/color/rgb-to-hex.html" target="_blank" rel="noopener noreferrer">RBG 转 Hex 工具</a>，给单元格、文字上颜色时会用到，因为 Excel 显示的是 RBG，但代码里是 Hex。</p>
]]></content:encoded>
    </item>
    <item>
      <title>mr.py</title>
      <link>https://levy.vip/python/mr.py.html</link>
      <guid>https://levy.vip/python/mr.py.html</guid>
      <source url="https://levy.vip/rss.xml">mr.py</source>
      <description>mr.py 操作 Gitlab MR 的命令行工具的源码与测试代码。</description>
      <pubDate>Mon, 05 Jun 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> <a href="http://mr.py" target="_blank" rel="noopener noreferrer">mr.py</a></h1>
<p><a href="/git/use-command-line-tool-to-manage-gitlab-merge-request.html" target="blank">操作 Gitlab MR 的命令行工具</a>的源码与测试代码。</p>
<!-- more -->
<h2> 源文件：<a href="http://main.py" target="_blank" rel="noopener noreferrer">main.py</a></h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 测试文件：test/test_mr.py</h2>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
    </item>
    <item>
      <title>单元测试概述</title>
      <link>https://levy.vip/software-testing/unit-testing-overview.html</link>
      <guid>https://levy.vip/software-testing/unit-testing-overview.html</guid>
      <source url="https://levy.vip/rss.xml">单元测试概述</source>
      <description>单元测试概述 Why 为什么要做单元测试？或者说，为什么要写测试代码？ 个人总结为以下两点： 测试左移，降低修复bug的成本 形成资产，方便回归测试，后续迭代重构、维护有保障</description>
      <pubDate>Fri, 28 Jul 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 单元测试概述</h1>
<h2> Why</h2>
<p>为什么要做单元测试？或者说，为什么要写测试代码？</p>
<p>个人总结为以下两点：</p>
<ol>
<li><a href="https://www.stickyminds.com/article/shift-left-approach-software-testing" target="_blank" rel="noopener noreferrer">测试左移</a>，降低修复bug的成本<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1690532448643-e09bebb0-66f2-49f9-8686-d4a8c6b5d590.png" alt=""></li>
<li>形成资产，方便回归测试，后续迭代重构、维护有保障</li>
</ol>
<!-- more -->
<p>以上两点，是研发人员写测试代码的本质理由，无论什么类型的测试代码、研发人员用的什么语言、框架都适用。</p>
<h2> What</h2>
<p>写测试代码究竟是写什么？</p>
<p>个人认为测试代码主要是为了搞清楚两件事：</p>
<ol>
<li>源码到底会不会在目标环境执行？</li>
<li>源码的执行结果是否符合预期？</li>
</ol>
<p>第一件事，引出了 code coverage 代码覆盖率的概念；第二件事，则引出了 assert 断言的概念。</p>
<h2> How</h2>
<h3> 测试代码的风格</h3>
<p><a href="https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80" target="_blank" rel="noopener noreferrer">AAA</a> 风格：</p>
<ol>
<li>组装参数</li>
<li>执行目标方法</li>
<li>执行断言</li>
</ol>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>尤其注意最后的断言，如果没有断言，不叫测试。</p>
<p>常见的错误就是，不写断言，而使用 <code>System.out.println()</code>来判断执行结果。<br>
这样做无法结合 CI 形成有效的自动化测试。 因为这种做法只能让编译通过，源码逻辑也许已经错误了，但测试结果仍然 100% 通过，这是没有意义的。</p>
<h3> 测试难点</h3>
<p>以函数的观点来看。</p>
<p>输入：</p>
<ol>
<li>内存数据</li>
<li>外部数据</li>
</ol>
<p>输出：</p>
<ol>
<li>内存数据</li>
<li>数据库</li>
<li>文件系统</li>
<li>网络调用</li>
</ol>
<p>单元测试从严格意义上来说需要满足三个No：</p>
<ol>
<li>No DB</li>
<li>No Network</li>
<li>No I/O</li>
</ol>
<p>由此，引出了 Mock 的概念及技术。作为单元测试，需要 Mock 依赖，准备好输入数据，并想办法在内存中验证外部输出。</p>
<p>也即，重要的是隔离依赖，让测试可重复执行。</p>
<h3> 常用工具</h3>
<ol>
<li><a href="https://junit.org/junit5/" target="_blank" rel="noopener noreferrer">Junit</a></li>
<li><a href="https://site.mockito.org/" target="_blank" rel="noopener noreferrer">Mocktio</a></li>
<li><a href="https://plugins.jetbrains.com/plugin/9471-testme" target="_blank" rel="noopener noreferrer">TestMe</a></li>
</ol>
<h2> Bad Examples</h2>
<p>以下是常见的错误测试示例，它们都不是合格的单元测试。</p>
<h3> 没有测试类</h3>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>经典错误：写一个 main 方法，把所有测试代码都放进去。这样做的后果是，无论是人还是机器，都不知道原来这里还有测试代码。</p>
<h3> 没有断言</h3>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>经典错误：（很可能是单纯地把测试代码从 main 方法移过来）没有断言，依赖人用肉眼判断输出正确与否。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这个例子虽然用上了 Mock 技术，但依赖掩盖不了没有断言的事实。这也许是为了达到测试覆盖率百分百而进行的投机取巧。</p>
<h3> 无法重复执行</h3>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果代码 Linux 环境运行怎么办？哪里来的 D 盘？</p>
<p>这种情况，正确的做法应该是把依赖的文件作为测试夹具，与测试代码一起放入版本控制中。<br>
<img src="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1695537638532-e8092338-3de2-4019-99a2-03bfb98f781f.png" alt=""><br>
参考代码如下：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div>]]></content:encoded>
      <enclosure url="https://raw.githubusercontent.com/levy9527/image-holder/main/md-image-kit/1690532448643-e09bebb0-66f2-49f9-8686-d4a8c6b5d590.png" type="image/png"/>
    </item>
    <item>
      <title>使用 RestAssured 进行 API 测试</title>
      <link>https://levy.vip/software-testing/use-RestAssured-for-api-testing.html</link>
      <guid>https://levy.vip/software-testing/use-RestAssured-for-api-testing.html</guid>
      <source url="https://levy.vip/rss.xml">使用 RestAssured 进行 API 测试</source>
      <description>使用 RestAssured 进行 API 测试 前言 本文将借助 RestAssured 工具，向大家介绍如何进行 API 测试，从而在团队中开启接口自动化之路。 本文的示例代码使用的是 Java 语言。尽管本文的首要读者是 Java 研发人员，但道理是相通的，其他语言的研发人员也能从中受益。 What 什么是 API 测试？简单来说，可以认为是针对 Controller 层的测试，但不是 Mock，而是会真实地处理请求，与数据库或外部服务进行交互。 Why 为什么要做 API 测试呢？</description>
      <pubDate>Fri, 09 Jun 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用 RestAssured 进行 API 测试</h1>
<h2> 前言</h2>
<p>本文将借助 RestAssured 工具，向大家介绍如何进行 API 测试，从而在团队中开启接口自动化之路。</p>
<p>本文的示例代码使用的是 Java 语言。尽管本文的首要读者是 Java 研发人员，但道理是相通的，其他语言的研发人员也能从中受益。</p>
<h2> What</h2>
<p>什么是 API 测试？简单来说，可以认为是针对 Controller 层的测试，但不是 Mock，而是会真实地处理请求，与数据库或外部服务进行交互。</p>
<h2> Why</h2>
<p>为什么要做 API 测试呢？</p>
<p>考虑有过这样的场景：</p>
<ul>
<li>加一个新功能，自测没问题，结果被测试人员发现一个旧模块出了问题，感到措手不及</li>
<li>后端写好了接口，前端还没开发好界面，于是感觉不方便自测，因为没有界面，只好催前端快去做页面</li>
</ul>
<p>API 测试就是来解决上述问题的。做 API 测试的原因有：</p>
<ul>
<li>必要性：做回归测试，避免添加新功能时破坏旧功能。</li>
<li>便利性：方便本地调试，不用部署到线上，依赖界面去测试。</li>
<li>资产化：让测试用例变成资产，与团队共享。</li>
</ul>
<p>当然，要做好 API 测试，还要接受这样的认知： 接口自动化测试并不仅仅是测试人员事情，研发人员也有责任把它做好。 否则，研发人员难免会觉得这不关我的事, 从而不愿意写这种代码。 建议研发人员从以下方便思考其好处，提升行动的积极性：</p>
<ul>
<li>减少阻塞，接口自测不再依赖前端</li>
<li>提高效率，本地就能自测，不用把应用部署到线上环境</li>
<li>提高质量，减少部署到研发环境、前端一调用接口就 500 的情况</li>
</ul>
<h2> 为什么不用Postman</h2>
<p>Postman 确实是符合直觉的接口调试的第一选项。 但注意，调试不等于测试。</p>
<p>Postman 在实践过程中，最大的问题在于，无法将测试用例有效地资产化:</p>
<ul>
<li>你会在 Postman 里写断言吗？很少吧，你其实是在用肉眼去检查接口成功与否，这本质还是手工测试</li>
<li>你的 Postman 数据能与团队共享吗？不能吧，大多数人的 Postman 数据是在本地的，也不会去付费创建一个团队以共享数据</li>
<li>你的 Postman 数据在有版本管理吗？没有吧，大多数人的 Postman 数据是与源代码分离的，不利于维护与管理</li>
</ul>
<p>另外，如果要与 CI 结合，Postman 的数据更适合使用 Node.js 的 <a href="https://github.com/postmanlabs/newman" target="_blank" rel="noopener noreferrer">Newman</a>。</p>
<p>考虑源代码是 Java，使用 RestAssured，编写 API 测试代码用同一种语言，可以减少使用者的心智负担较轻；并且与源代码放在同一个 Git 仓库中，易于管理。</p>
<p>因此，我仍然会使用 Postman，但更多是把它应用在出现线上问题时，直接复制一个 cURL 用来复现、排查问题的情况。</p>
<h2> 安装</h2>
<p>下面将介绍如何用 Maven 安装 RestAssured。</p>
<p>复制以下内容到 pom.xml 即可。</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>安装完成后，重启 Spring 容器。</p>
<p>如果安装依赖不成功，可以进行以下检查：</p>
<ul>
<li>显式指定 json-path 与 xml-path 的版本，并排除其他测试包(如 sping-boot-starter-test) 对 json-path 的依赖</li>
<li>声明放在 JUnit 前面</li>
</ul>
<h2> 快速上手</h2>
<p>语法结构为： given()、when()、then()</p>
<div class="language-xml line-numbers-mode" data-ext="xml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 通用设置</h2>
<p>以下代码可直接复制到 Java 测试类中。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>getToken方法的示例代码：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 请求示例</h2>
<p>下面是一个 GET 请求示例，根据响应体进行断言：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>下面是一个更完整的POST示例，包含了：</p>
<ul>
<li>组装数据</li>
<li>设置body</li>
<li>设置query</li>
<li>判断响应体的数据结构</li>
</ul>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>提醒，在运行测试代码前，需要做两件事：</p>
<ul>
<li>一定保证 Web 服务已启动，因为这不是 Mock，而是会发送真实的请求。</li>
<li>正确配置了环境变量 TOKEN。如果使用 IDEA，可以编辑运行配置，在环境变量里注入类似代码：TOKEN=Bearer xxx</li>
</ul>
<h2> 接口依赖</h2>
<p>有时在请求接口 B 之前，需要请求接口 A，于是就产生了接口依赖：B 依赖了 A。</p>
<p>此时可以使用 extract() 及 path() 获取请求 A 返回的数据。</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 上传示例</h2>
<p>RestAssured 很强大，还能处理上传与下载的请求，简直让人“爱了爱了”。 下面是具体的示例：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果想在传文件的基础上，还传其他参数，可以这样写：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>对应的前端请求代码为(记录一下，以备不时之需😃)：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 下载示例</h2>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>看到全部用例都执行成功，非常爽快！<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/resetassured-download.png" alt="resetassured-download"></p>
<h2> 持续集成</h2>
<p>以集成 Gitlab CI 为例，其核心思路就是在 CI 环境运行 <code>mvn test</code>。</p>
<p>具体做法可以参考笔者的<a href="../git/gitlab-ci#%E9%9B%86%E6%88%90%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95">Gitlab CI文章</a>。</p>
<h2> 其他问题</h2>
<h3> 为什么不用 Pytest</h3>
<p>如果编码代码的人员是测试人员，那可能首选 Pytest。但本文面向的读者的 Java 研发——既写 API，也写相应的测试代码。故选型理由参考前面 为什么不用Postman 的回答。</p>
<h3> 这也是单元测试吗</h3>
<p>不是。运行上述测试代码，如果是测试本地接口，需要先在本地启动 Spring 容器；如果是测试线上接口，则需要先把应用部署到线上。因此，这是集成测试。</p>
<h2> 参考资料</h2>
<p>官方文档：<a href="https://github.com/rest-assured/rest-assured/wiki/Usage#examples" target="_blank" rel="noopener noreferrer">https://github.com/rest-assured/rest-assured/wiki/Usage</a></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/resetassured-download.png" type="image/png"/>
    </item>
    <item>
      <title>使用 Cypress 进行端对端测试</title>
      <link>https://levy.vip/software-testing/use-cypress-for-e2e-testing.html</link>
      <guid>https://levy.vip/software-testing/use-cypress-for-e2e-testing.html</guid>
      <source url="https://levy.vip/rss.xml">使用 Cypress 进行端对端测试</source>
      <description>使用 Cypress 进行端对端测试 为什么写端对端测试 写端对端测试代码的最大好处就是，把相关的用例变成可执行的代码，成为项目的资产；结合CI系统，可在后续研发维护过程中，将一部分测试过程自动化，减少重复的手工劳动，既保障质量，又提高效率。 谁来写呢？本文的目标读者是前端研发人员，因而相关测试代码是由前端同学去编写的。 为什么用 Cypress 文档齐全，生态好，对 JavaScript 友好，可简单上手。更多详见：why-cypress</description>
      <pubDate>Tue, 08 Dec 2020 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用 Cypress 进行端对端测试</h1>
<h2> 为什么写端对端测试</h2>
<p>写端对端测试代码的最大好处就是，把相关的用例变成可执行的代码，成为项目的资产；结合CI系统，可在后续研发维护过程中，将一部分测试过程自动化，减少重复的手工劳动，既保障质量，又提高效率。</p>
<p>谁来写呢？本文的目标读者是前端研发人员，因而相关测试代码是由前端同学去编写的。</p>
<h2> 为什么用 Cypress</h2>
<p>文档齐全，生态好，对 JavaScript 友好，可简单上手。更多详见：<a href="https://docs.cypress.io/guides/overview/why-cypress.html" target="_blank" rel="noopener noreferrer">why-cypress</a></p>
<p>缺点：全英文档</p>
<h2> 快速开始</h2>
<h3> 安装</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>下载完依赖后，cypress 还会再从网络下载二进制执行包。安装完成后会在本地全局缓存一份二进制执行包，那么这台机器上所有项目都可以使用这份缓存。<a href="https://docs.cypress.io/guides/getting-started/installing-cypress.html#npm-install" target="_blank" rel="noopener noreferrer">文档参考</a></p>
<p>一般而言，国内用户都会在上述过程中卡住，最好在命令行设置网络代理后再下载（懂的自然懂）。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果是在 CI 环境，记得缓存 cypress binary。</p>
<p>安装完后，修改 package.json</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 加速下载</h3>
<p>因为安装时，需要科学上网，如果不想设置代理，也能加速下载安装。可以自己先下载官方提供的二进制 <a href="https://download.cypress.io/desktop.json" target="_blank" rel="noopener noreferrer">cypress.zip</a>，再上传至自己的 OSS。</p>
<p>则安装 Cypress 时，设置 <code>CYPRESS_INSTALL_BINARY</code> 指向对应的地址即可。如</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>或使用淘宝镜像，缺点是可能包不是最新的。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>或这样写</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 目录结构</h3>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494179328.png" alt="image.png"><br>
推荐结构如上图所示的目录结构：</p>
<ul>
<li>cypress 相关的内容放到 test/e2e 文件夹下。与单元测试的 unit 文件夹区分开来</li>
<li>config 存放不同环境下的<a href="https://docs.cypress.io/guides/guides/environment-variables.html#Setting" target="_blank" rel="noopener noreferrer">变量</a>，如 dev/uat 环境的 baseUrl 是不同的，可分别在 config 里</li>
<li>fixtures 存放测试 mock 数据</li>
<li>integration 存放的就是 cypress 的测试用例了，命名规范同 jest：${name}.spec.js</li>
<li>plugins 存放的是相关插件</li>
<li>support 存放自定义的 cypress 命令</li>
</ul>
<p>可以根据要求，修改文件夹目录结构，只要在 cypress.json 里配置好即可：</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意，如果不显示声明这些配置，每次执行 cypress 命令都会自动生成相应的示例文件</p>
</blockquote>
<p>cypress.json 是放在项目根目录下的默认配置文件，全部配置项可<a href="https://docs.cypress.io/guides/references/configuration.html#Options" target="_blank" rel="noopener noreferrer">查看文档</a></p>
<blockquote>
<p>通过 <a href="https://github.com/FEMessage/create-nuxt-app" target="_blank" rel="noopener noreferrer">FEMessage/create-nuxt-app</a> 生成的项目默认是使用上面的配置</p>
</blockquote>
<h3> 与 Jest 协同工作</h3>
<p>当项目也在使用 jest 进行单元测试时，有两个注意点。</p>
<h4> ESLint 配置</h4>
<p>推荐项目中存在三份 eslint 配置文件：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>当然，还要安装相应的依赖：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h4> 测试目录</h4>
<p>两个工具都需要明确指定各自的测试目录。</p>
<p>cypress 的测试目录可通过上文所说的  cypress.json 指定。</p>
<p>jest 测试目录则可通过在 jest.config.js 里指定：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 检查依赖及生产安装依赖命令</h3>
<p>请确保生产安装依赖命令为</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>上述命令，只会安装 package.json 里声明的 <code>dependencies</code>&nbsp;依赖，避免因为下载 Cypress 而超时。</p>
<p>因此，也要确保项目中 package.json 中的 <code>dependencies</code>&nbsp;<code>devDependencies</code>&nbsp;等声明是正确的。</p>
<h3> 第一个用例</h3>
<p>新建 common.spec.js</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>上面的示例覆盖了三个 cypress 常用命令：</p>
<ul>
<li>跳转页面</li>
<li>获取元素</li>
<li>断言</li>
</ul>
<p>这里说一下 <code>should</code>&nbsp;命令，它相当于是 <code>expect.to</code>&nbsp;的简写。<br>
如： <code>expect($input).to.be.disabled</code>&nbsp;可写成 <code>get($input).should('be.disabled')</code></p>
<p>更多命令，可<a href="https://docs.cypress.io/api/commands/get.html#Syntax" target="_blank" rel="noopener noreferrer">查看API</a><br>
常见断言，可<a href="https://docs.cypress.io/guides/references/assertions.html#Common-Assertions" target="_blank" rel="noopener noreferrer">查看文档</a></p>
<p>如果想获得代码提示、代码补全，需在开头添加如下语句，（Webstorm不需要此配置）<a href="https://docs.cypress.io/zh-cn/guides/tooling/intelligent-code-completion.html#" target="_blank" rel="noopener noreferrer">参考文档</a></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><hr>
<p>执行 <code>yarn e2e</code>&nbsp;，出现弹窗<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494184473.png" alt="image.png"><br>
点击文件，即会执行用例。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494186469.png" alt="image.png"></p>
<h3> 更复杂的示例</h3>
<div class="language-text line-numbers-mode" data-ext="text"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 结合TypeScript</h2>
<p>在 e2e 目录添加 tsconfig.json，内容如下：</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>在 e2e/support 添加 index.d.ts，如果有自定义命令的话</p>
<div class="language-typescript line-numbers-mode" data-ext="ts"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>将 e2e/integration/xxx.spec.js 重命名为  e2e/integration/xxx.spec.ts，并添加如下2行内容：</p>
<div class="language-typescript line-numbers-mode" data-ext="ts"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 持续集成</h2>
<p>持续集成的第一步要选择合适的包含 Cypress 的<a href="https://github.com/cypress-io/cypress-docker-images" target="_blank" rel="noopener noreferrer">镜像</a>。</p>
<blockquote>
<p>注意自身的 Node 版本选择合适的镜像。</p>
</blockquote>
<p>另一方面，一般 CI 环境下执行的是 <code>cypress run</code>&nbsp;命令。</p>
<blockquote>
<p>run 与 open 的不同之处在于，run 默认不会启动浏览器界面，使用的是 headless 模式执行用例。</p>
</blockquote>
<p>同时，需要安装 Cypress 时，需要设置环境变量 <code>CYPRESS_INSTALL_BINARY</code></p>
<p>最后，还是要强调一下，在生产安装依赖环节，使用如下命令安装依赖，则不会安装 Cypress 依赖</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 直接运行 Cypress</h3>
<p>直接运行 Cypress 的场景是，e2e 作为 CI 的最后一个<a href="https://docs.gitlab.com/ee/ci/yaml/README.html#stages" target="_blank" rel="noopener noreferrer">阶段</a>，当应用完成部署后，再对应用运行线上的测试。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>不推荐选择 <code>cypress/included</code> 镜像直接执行 Cypress 命令，因为很可能会遇到以下问题。<br>
当然，如果自己的用例写的不好，也很可能会出现下面的问题。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494188361.png" alt="image.png"></p>
<h3> 使用 start-server-and-test</h3>
<p>如果需要本地起个 localhost 服务，然后再运行 cypress。那么可以合作官方推荐的 <code>start-server-and-test</code>&nbsp;模块。它在 CI 上的执行顺序是：</p>
<ol>
<li>在系统后台执行拉起本地服务的命令</li>
<li>使用 wait-on 模块监听并等待该本地服务响应 200</li>
<li>执行 test 命令，完成并退出</li>
<li>CI 环境此时会自动关闭所有后台进程并退出</li>
</ol>
<p>下面以 <a href="http://gitlab.com/" target="_blank" rel="noopener noreferrer">gitlab.com</a> 为例，展示执行完 gitlab jobs 后可看到 test 记录和下载测试产物（视频及截图）</p>
<ul>
<li>package.json</li>
</ul>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>.gitlab-ci.yml</li>
</ul>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> This job is stuck</h3>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494190870.png" alt="image.png">如果是自建的 gitlab, 可能会遇到这个问题。<br>
这是任务没有打对标签，导致无法给任务分配对应的 Runner。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494192801.png" alt="image.png"><br>
进入上图所示页面，注意找到可使用的 Runner<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494195056.png" alt="image.png"><br>
如上图所示， <code>docker</code>&nbsp;标签对应的 Runner 是处于激活状态的，则在 CI 文件里配置即可</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> Cypress Dashbord</h3>
<p>Cypress 官方提供了一个测试记录托管服务。在 CI 命令中，只需要加上 <code>--record --key $key</code>&nbsp;即可。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>CI 日志如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494197613.png" alt="image.png"><br>
更多介绍请查阅<a href="https://docs.cypress.io/guides/dashboard/introduction.html#Features" target="_blank" rel="noopener noreferrer">官方文档</a></p>
<h2> 总结</h2>
<p>cypress 比较适合写一个流程测试。一般情况下，只需要把整个正常流程操作使用 cypress 记录下来即可。</p>
<p>一个流程可能长这样：创建-&gt;验证-&gt;修改-&gt;验证-&gt;删除-&gt;验证。那我们就可以根据该流程，模拟填写合法数据，模拟点击提交按钮，检查页面是否有相应内容即可。</p>
<p>这样，每次开发新功能后，编写测试用例，再跑 Cypress，就能把一部分的回归测试自动化了，保证完成新功能的同时，原有功能最低限度地保持可用。</p>
<h2> 拓展阅读</h2>
<ul>
<li><a href="https://www.yuque.com/femessage/fwrngg/arlhoq" target="_blank" rel="noopener noreferrer">Cypress 实战总结</a></li>
</ul>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-testing/1686494179328.png" type="image/png"/>
    </item>
    <item>
      <title>使用 Jest 实践测试驱动开发</title>
      <link>https://levy.vip/software-testing/use-jest-for-test-driven-development.html</link>
      <guid>https://levy.vip/software-testing/use-jest-for-test-driven-development.html</guid>
      <source url="https://levy.vip/rss.xml">使用 Jest 实践测试驱动开发</source>
      <description>使用 Jest 实践测试驱动开发 前言 本文将使用jest进行测试驱动开发的示例，源码在github。 旨在说明在开发中引入单元测试后开发过程，以及测试先行的开发思路。</description>
      <pubDate>Sun, 21 Apr 2019 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用 Jest 实践测试驱动开发</h1>
<h2> 前言</h2>
<p>本文将使用<a href="https://jestjs.io/docs/en/getting-started" target="_blank" rel="noopener noreferrer">jest</a>进行测试驱动开发的示例，源码在<a href="https://github.com/levy9527/jest-tdd-demo" target="_blank" rel="noopener noreferrer">github</a>。<br>
旨在说明在开发中引入单元测试后开发过程，以及测试先行的开发思路。</p>
<p>本文的重点是过程以及思维方法，框架以及用法不是重点。</p>
<p>本文使用的编程语言是javascript，思路对其他语言也是适用的。</p>
<p>本文主要以函数作为测试对象。</p>
<h2> 环境搭建</h2>
<p>假设项目结构为</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>安装依赖</li>
</ul>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><ul>
<li>打开package.json, 修改scripts字段</li>
</ul>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>之后把测试文件放在test文件夹下，使用<code>yarn test</code> 即可查看测试结果</p>
<h2> 开发</h2>
<p>现在要开发一个函数，根据传入的文件名判断是否为shell文件。</p>
<p>先做好约定：</p>
<ol>
<li>shell文件应该以 <code>.sh</code> 结尾</li>
<li>shell文件不以 <code>.</code> 开头</li>
<li>函数为名 <code>isShellFile</code></li>
</ol>
<p>下面来看下开发步骤是怎么样的。</p>
<h3> 文件初始化</h3>
<p>在src目录下新建 <code>isShellFile.js</code></p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>然后一行代码也不写，在test目录下新建 <code>isShellFile.test.js</code></p>
<p>可以注意到，测试文件的名与源文件名类似，只是中间多了个 <code>.test</code></p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 第一个用例</h3>
<p>打开测试文件 <code>test/isShellFile.test.js</code> ，编写第一个用例，也是最普通的一个: <code>bash.sh</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>运行 <code>yarn test</code> , 结果如下：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>失败是意料之中的，因为 <code>src/isShellFile.js</code> 一行代码也没写，所以测试代码中第5行 <code>isShellFile</code> 无法进行函数调用。</p>
<p>完善源文件<code>src/isShellFile.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>这样 <code>isShellFile</code> 就可以作为函数被调用了。</p>
<p>再运行 <code>yarn test</code></p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>又报错了，但这次报错原因跟上次不同，说明有进步。</p>
<p>这次报错原因是，期望函数调用返回值为真 , 但实际没有返回真 。</p>
<p>这是当然的，因为在源文件中，根本没有写返回语句。</p>
<p>为了让测试通过，修改 <code>src/isShellFile.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>运行 <code>yarn test</code> , 测试通过了！</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>把上述修改，提交到版本控制系统中。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 第二个用例</h3>
<p>观察我们的测试用例，发现太简单了，只有正面的用例，没有反面的、异常的用例</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>在 <code>test/isShellFile.test.js</code> 添加一个反面的用例</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>运行 <code>yarn test</code></p>
<p>(可以发现，在开发过程中需要反复执行上述命令，有个偷懒的办法，执行<code>yarn test --watch</code>，即可监听文件变化，自动执行测试用例)</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>报错了，期望返回假，但函数返回的是真。这是因为，源文件中， <code>isShellFile</code> 函数永远返回真！</p>
<p>完善 <code>src/isShellFile.js</code> 逻辑</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试通过了</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>把上述修改提交到版本控制系统</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 第三个用例</h3>
<p>我们再添加一个用例，这次考虑特殊情况： <code>.sh</code> 这种文件，不算是shell文件。</p>
<p>修改 <code>test/isShellFile.test.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试不通过</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>说明逻辑待完善，修改 <code>src/isShellFile.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试通过(为精简文章内容，后面不再展示测试通过的输出)，提交代码。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 第四个用例</h3>
<p>按照第三个用例的逻辑， <code>.bash.sh</code> 也不应该是shell文件，那么函数是否能正确判断呢，测试便知。</p>
<p>修改 <code>test/isShellFile.test.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>测试不通过</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>说明逻辑待完善，修改 <code>src/isShellFile.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试通过，提交代码。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 第五个用例</h3>
<p>再考虑一种情况，如果 <code>.sh</code> 出现在中间呢？如 <code>bash.sh.txt</code> , 它不应该是shell文件，来看看函数是否能通过测试。</p>
<p>修改 <code>test/isShellFile.test.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>测试不通过</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>说明逻辑待完善，修改 <code>src/isShellFile.js</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试通过，提交代码。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 重构</h3>
<p>我们来观察目前 <code>src/isShellFile.js</code> 的函数逻辑</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>对于 <code>.bashrc</code> 这样的文件，并不是shell文件，因为它是以 <code>.</code> 开头的。</p>
<p>则通过 <code>filename.startsWith('.')</code> 判断即可，前面的函数调用 <code>filename.lastIndexOf(".")</code> 是多余的。也即，目前的函数判断逻辑不够简明。</p>
<p>下面是一种优化思路：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>测试通过，提交代码</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>注意，这个重构示例的重点是：</p>
<ol>
<li>先完成功能，再重构</li>
<li>重构必须要有测试用例，且确保重构后全部测试用例通过</li>
</ol>
<p>至于其他方面，见仁见智，并不是重点。</p>
<h2> 结论</h2>
<p>本文通过代码实例，践行了测试先行的理念。</p>
<p>文中的代码实现不是重点，而是开发过程。</p>
<p>文中 <a href="#Pv5Ni">文件初始化</a> 及 <a href="#Pv5Ni">第一个用例</a> 的内容，尤其值得回味，它体现了两个思路：</p>
<ul>
<li>总是在有一个失败的单元测试后才开始编码</li>
<li>用必要的最小代码让测试通过</li>
</ul>
<p>总的来看，TDD总是处于一个循环中：</p>
<ol>
<li>编写用例</li>
<li>测试失败</li>
<li>编写代码</li>
<li>测试成功</li>
<li>提交代码</li>
<li>重复以上</li>
</ol>
<p>通过这样，功能的实现每次都是最小成本的，功能也是有步骤地、通过迭代完成的，而不是一步登天。</p>
<p>更关键的是，完善的测试用例，是开发者的“守护天使”，有了它们，以后在添加新功能时，修改/重构代码都有了可靠的保障，让开发者可以充满信心，code with confidence😎！</p>
<p>另外，测试用例延伸出的思考还有：</p>
<ol>
<li>不需要追求完美软件，不用过分考虑将来的变化：先设计能符合当前需求的用例，再编码通过测试用例即可。将来有变化，重构代码即可，因为有用例，不用担心改错了。</li>
<li>重构的前提，是存在完善的测试用例。如果没有用例，只有源码，是不敢谈轻易重构，否则就是在走钢丝。</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>下一代 UI 自动化测试工具 Playwright</title>
      <link>https://levy.vip/software-testing/use-playwright-for-ui-testing.html</link>
      <guid>https://levy.vip/software-testing/use-playwright-for-ui-testing.html</guid>
      <source url="https://levy.vip/rss.xml">下一代 UI 自动化测试工具 Playwright</source>
      <description>下一代 UI 自动化测试工具 Playwright 前言 Playwright 是微软于 2020 年发布的一款 E2E testing 工具，跟社区成熟的 Cypress 相比，稍显年轻。然而 Playwright 的主要优势有： 支持多语言：Node.js、Java、Python，也即它并非是前端工程师专属的工具 开箱即用的代码生成功能（Cypress 现在也支持，不过要修改配置或安装插件） 另外，Playwright 的安装没什么门槛，不像 Cypress 可能需要黑魔法。</description>
      <pubDate>Sun, 07 May 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 下一代 UI 自动化测试工具 Playwright</h1>
<h2> 前言</h2>
<p>Playwright 是微软于 2020 年发布的一款 E2E testing 工具，跟社区成熟的 Cypress 相比，稍显年轻。然而 Playwright 的主要优势有：</p>
<ol>
<li>支持多语言：Node.js、Java、Python，也即它并非是前端工程师专属的工具</li>
<li>开箱即用的代码生成功能（Cypress 现在也支持，不过要修改配置或安装插件）</li>
</ol>
<p>另外，Playwright 的安装没什么门槛，不像 Cypress 可能需要黑魔法。</p>
<p>综上所述，笔者认为 Playwright 是值得在研发过程中引入的一款测试工具，它可以帮助研发、测试团队较平滑地走上自动化测试之路。它适用的典型场景之一，就是做回归测试——测试人员再也不用在界面上使用鼠标进行“点点点”，解放双手，提高测试效率。</p>
<h2> 安装</h2>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>根据命令提示，输入如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683274667037.png" alt="image.png"><br>
默认会下载所有浏览器，如果没有浏览器兼容性测试的需求，推荐如上图所示，手动安装一个浏览器。</p>
<p>以安装 chromium 为例，相应操作步骤如下：</p>
<ol>
<li>修改配置</li>
</ol>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>注释掉以下内容：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683274759403.png" alt="image.png"></p>
<ol start="2">
<li>安装浏览器</li>
</ol>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>等待一段时间即可，如果失败，请重试。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683274769748.png" alt="image.png"></p>
<p>推荐再安装 <a href="https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright" target="_blank" rel="noopener noreferrer">VS Code 插件</a>，获取更好的使用体验。</p>
<h2> 使用</h2>
<h3> 代码生成</h3>
<p>虽然可以参考 <code>example.spec.ts</code>去编写测试用例，但这不是 Playwright 独特之处。Playwright 最引入注目的，是代码生成功能。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>上述命令会打开两个浏览器窗口：</p>
<ol>
<li>一个是普通的浏览器界面</li>
<li>另一个是代码生成界面，在前一个窗口进行的任何操作，都会生成相应的代码</li>
</ol>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683275050705.png" alt="image.png"><br>
虽然默认生成代码是 Javascript，但可以选择切换语言：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683275136008.png" alt="image.png"><br>
注意到可以生成 Pytest 的代码，对测试工程师来说，简直是福音。这也提示我们，Playwright 既可以由前端研发来使用，也可以由测试人员来使用，并不限制使用者的职业身份。</p>
<p>点击"Copy"按钮<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683276292010.png" alt="image.png"><br>
然后打开代码编辑器，把代码复制进去即可。</p>
<p>点击"Clear"按钮，<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277483800.png" alt="image.png"><br>
可以清空本次操作生成的代码，从而开始进行下一次操作的代码生成。</p>
<p>如果是使用 VS Code 插件，点击"Record new"即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277490131.png" alt="image.png"></p>
<h3> 修改代码</h3>
<p>生成的代码，最好还是检查一下，也许需要去掉一些多余的操作记录。<br>
如下面的代码，<code>Tab</code>的操作只是人工操作时为了方便而进行的按键，对机器而言，是多余的，应该去掉。</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果想基于现在的测试代码，继续生成新的代码，可以使用 VS Code：</p>
<ul>
<li>把光标放到测试用例的最后一行</li>
<li>点击"Record at cursor"，即可继续录制</li>
</ul>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277555357.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<h3> 执行用例</h3>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277566741.png" alt="image.png"><br>
如果用例失败了，想查看到底哪里错了，可以用以下命令显示浏览器，查看用例执行过程：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果是使用 VS Code，直接点击运行用例即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277575650.png" alt="image.png"><br>
勾选左下角的"Show broswer"，即可显示浏览器。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277598218.png" alt="image.png"></p>
<h3> 调试用例</h3>
<p>对于失败的用例，如何 debug呢？添加 --debug 参数即可。</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>点击"Step over" 即可执行下一行代码。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277605265.png" alt="image.png"></p>
<p>如果是使用 VS Code，找到相应的用例，右键出现"Debug Test"，点击即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277678957.png" alt="image.png"></p>
<h3> 查看报告</h3>
<p>在执行完用例后，本地会生成目录 <code>playwright-report</code>，可以通过以下命令查看测试报告</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277713874.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<h2> 常见场景与解决方案</h2>
<h3> 应用登录</h3>
<p>下面给出一个自动登录、并保存用户数据的解决方案。</p>
<p>先创建文件夹，并让 git 忽略它</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>创建 login.ts</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>修改 playwright.config.ts</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 环境变量</h3>
<p>通常不同的环境 url 前缀是不同的，我们希望通过变量注入的方式来适配不同的环境，而不是硬编码在测试用例里。</p>
<p>我们可以借助模块 <code>dotenv</code>， 来配置 baseURL。</p>
<p>首先安装该模块：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>在项目根目录新建 <code>.env</code> 文件：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>输入以下内容：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>修改 playwright.config.ts</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>然后，测试用例里，url 路径只写相对路径 <code>/path</code>，程序会自动拼接成完整的路径：<code>http://dev-domain.company.com/path</code></p>
<p>以后要测试不同的环境时，只需要修改 <code>.env</code> 的变量值即可。</p>
<h3> 超时时间</h3>
<p>默认的超时时间不太够用，建议修改 playwright.config.ts:</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>同时要注意拆分用例，没有依赖关系的用例建议拆分开来，避免用例执行时间过长超时。</p>
<h3> 元素选择</h3>
<p>人们对元素选择的第一反应是使用 CSS 或 XPath，但 Playwright 并不鼓励这样使用，因为这些选择器容易改变。较为好的办法是，为测试元素添加专门的属性 testid，如下所示：</p>
<div class="language-html line-numbers-mode" data-ext="html"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>然后通过下列方式进行选择：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>当然，这种方式会对源代码有侵入。更为折衷的方式是，优先使用下列<a href="https://playwright.dev/docs/locators#locate-by-role" target="_blank" rel="noopener noreferrer">官方推荐的方法</a>进行元素选择，最后再使用业务 class，<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277746417.png" alt="image.png"></p>
<p>这里值得一提的是，业务class是针对 Tailwind CSS 这种“解构主义”的纯样式class而言的。你会发现，如果全是 Tailwind 的class，没有业务样式，E2E测试代码很不好写。</p>
<p>更复杂的示例：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>如果是使用 VS Code，有辅助办法：</p>
<ol>
<li>点击“Pick locator”<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683421157907.png" alt="image.png"></li>
<li>切换到浏览器界面，点击目标元素</li>
<li>切回 VS Code，即可看到相应的元素选择代码<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683421163861.png" alt="image.png"></li>
</ol>
<h3> 声明断言 &amp;&amp; 检查元素是否存在</h3>
<p>生成的代码是没有断言的，因此，很有可能页面报错了，用例执行报告仍然显示成功。为避免这种情况，每个用例至少要有一句断言。</p>
<p>常用的断言是，检查某一元素是否存在：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>当然也可以用以下方法，这取决于元素是否可见（元素存在，未必可见）。</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>注意，Node.js 才可以在 locator 里写 CSS 选择器，如果是 Python, 需要使用<code>query_selector</code>：</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>更多断言写法，参考官方文档(<a href="https://playwright.dev/docs/test-assertions#auto-retrying-assertions" target="_blank" rel="noopener noreferrer">https://playwright.dev/docs/test-assertions#auto-retrying-assertions</a>)。</p>
<h3> 获取第n个元素</h3>
<p>通过定位器得到的元素可能不止一个，可以使用以下代码获得具体某一个元素：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 遍历元素</h3>
<p>使用定位器后，调用<code>.all()</code></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 获取元素属性</h3>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h3> 判断子元素数量</h3>
<p>使用 <code>$</code> 及 <code>$$</code> <a href="https://playwright.dev/docs/api/class-elementhandle#element-handle-query-selector" target="_blank" rel="noopener noreferrer">元素选择器</a></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><h3> 鼠标悬浮</h3>
<p>有些元素是在鼠标悬浮时才显示或创建的，可以使用以下代码</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>这里有个问题，自己怎么知道悬浮后显示的元素是否正确地定位到了呢？可以通过下面的小技巧：</p>
<ol>
<li>切换到 codegen 打开的浏览器页面</li>
<li>打开网页控制台（按F12)</li>
<li>鼠标悬浮在目标元素上面，然后右键，如下图所示</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277759344.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<ol start="4">
<li>点击控制台内部，则此时元素不会丢失 hover 状态，如下图所示</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277780882.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<ol start="5">
<li>切换到 VS Code</li>
</ol>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277793308.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<h3> 操作剪贴板</h3>
<p>读写剪贴板需要设置权限，下面给出一个判断是否成功从剪贴板获取特定文本的测试用例：</p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 持续集成</h2>
<p>以 Gitlab CI 为例，说明 Playwright 如何集成进 CI 流水线中。其他方式如 Jenkins，请<a href="https://playwright.dev/docs/ci#jenkins" target="_blank" rel="noopener noreferrer">参考文档</a>。</p>
<p>首先确保已安装 Gitlab Runner 并成功注册，具体操作可以参考<a href="/git/gitlab-ci.html#%E5%AE%89%E8%A3%85gitlab-runner" target="blank">安装文档</a>。</p>
<p>端对端的测试耗时较长，并且对环境的稳定性有要求，作为回归测试的实践时，一般倾向于借助定时任务跑测试用例。</p>
<p>新建调度：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683436020492.png" alt="image.png"><br>
设置调度时间及环境变量：</p>
<ul>
<li>每 6 小时跑一次</li>
<li>e2e 环境变量的值为 true</li>
</ul>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683436027996.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<p>现在可以开始编写 .gitlab-ci.yml，下面只给出测试相关的配置。</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>注意点：</p>
<ol>
<li>entrypoint 解决的是 <a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27614" target="_blank" rel="noopener noreferrer">shell not found</a> 问题</li>
<li>--ignore-engines 可以在不修改源码的情况下避免安装失败</li>
<li>只有定时调度才会触发该任务的执行</li>
</ol>
<p>再修改 Gitlab Runner 的配置，解决<a href="https://github.com/nodejs/help/issues/1754#issuecomment-1260462271" target="_blank" rel="noopener noreferrer">yarn命令无法运行</a>的问题：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>根据 token 找到对应的 Runner 配置，按照下图所示，把红框处的值设置成 <code>true</code><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277800200.png" alt="image.png"><br>
config.toml 里面可能会有多个 Runner 配置，如何找到要修改哪一个呢？<br>
可以在项目界面，根据下图所示的 token（w8exPBfA） 去查找。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683277808112.png" alt="image.png"></p>
<p>修改完后，重启 Gitlab Runner</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>最后，使用 Chromium 可能会出现内存超出限制的问题，需要对 Docker 设置 --ipc=host，配置 .gitlab-ci.yml 如下：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 其他</h2>
<h3> <strong><a href="http://setup.py" target="_blank" rel="noopener noreferrer">setup.py</a> bdist_wheel did not run successfully</strong></h3>
<p>Python安装时，可能会再现此错误。解决方案如下：</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>之后，再安装</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>最后，重新安装Playwright即可</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div>]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/software-test/1683274667037.png" type="image/png"/>
    </item>
    <item>
      <title>使用 Postman 进行 API 测试</title>
      <link>https://levy.vip/software-testing/use-postman-for-api-testing.html</link>
      <guid>https://levy.vip/software-testing/use-postman-for-api-testing.html</guid>
      <source url="https://levy.vip/rss.xml">使用 Postman 进行 API 测试</source>
      <description>使用 Postman 进行 API 测试 前言 虽然之前分享过 RestAssured 进行接口测试的教程，但实践起来，会有阻碍：研发同学还是对 Postman 更熟悉，更倾向于使用 Postman 调试接口，而不是写 Java 代码对 Controller 层进行测试。 而笔者在针对旧的 Java 项目添加接口测试时，又遇到了另一个问题：那就是由于模块依赖，进行接口测试时，还在把旧的测试代码一并带上。虽然说有办法解决，但究竟是麻烦不断。 还有就是，Java 的类型检查，在写接口测试时十分束缚手脚。如下述代码：</description>
      <pubDate>Wed, 13 Sep 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用 Postman 进行 API 测试</h1>
<h2> 前言</h2>
<p>虽然之前分享过 RestAssured 进行接口测试的教程，但实践起来，会有阻碍：研发同学还是对 Postman 更熟悉，更倾向于使用 Postman 调试接口，而不是写 Java 代码对 Controller 层进行测试。</p>
<p>而笔者在针对旧的 Java 项目添加接口测试时，又遇到了另一个问题：那就是由于模块依赖，进行接口测试时，还在把旧的测试代码一并带上。虽然说有办法解决，但究竟是麻烦不断。</p>
<p>还有就是，Java 的类型检查，在写接口测试时十分束缚手脚。如下述代码：</p>
<!-- more -->
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>笔者实在忍不住吐槽了一番：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694596433278-eb3e5566-0126-4e93-9dfe-a9c4d8edae51.png" alt=""></p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694596460562-b472ca9d-8b73-4e1f-b5a5-7c9c1fb8253a.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>笔者思前想后，最终还是放弃测试代码要与源码使用统一技术栈的构想，再次搬出 Postman 作为接口自动化测试的工具。</p>
<h2> 本地调试</h2>
<h3> 接口集合</h3>
<p>新建一个 collection，然后再在里面新建接口。</p>
<p>新建方法多种多样，可以手工新建，也可以 curl 导入，也可以从 swagger 导入。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694595120400-0e37bf26-405b-412c-b54f-7057cf49ab20.png" alt=""></p>
<h3> 从 cURL 导入</h3>
<p>对于已经上线的接口，使用 cURL 导入非常方便，省去了拼接参数的过程。</p>
<p>下面以获取 token 接口为例进行说明。</p>
<p>打开登录页面，打开浏览器控制台（按 F12），点击登录按钮，找到获取 token 的接口，然后右键 -&gt; Copy -&gt; Copy as cURL（bash）。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694737892655-0ebc74d0-17de-4972-9411-e36697fd5637.png" alt=""><br>
再打开 Postman，点击 Import 即可导入接口。</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694738073586-9de6f40e-6ddd-4b0a-b330-b807f9cb851b.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<h3> 环境变量</h3>
<p><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602746875957-dc9090e7-900e-4352-91f7-039792e2a9e0.png" alt=""><br>
点击右上角红框处，即可设置变量，需要先设置环境名。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602747172023-7b96aa77-39bc-4881-b1e9-36d5535c92f1.png" alt=""></p>
<p>一般设置 current value 即可，则运行 postman 时使用的就是该值。</p>
<p>如果有多套环境，就点击复制，再修改环境名及包含变量的 current value 即可。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602747475793-1f85e976-8189-44b7-9667-f1a195ff8c35.png" alt=""></p>
<p>使用 <code>{{var}}</code> 的形式引用变量，可在 url 及 body 处引用环境变量 <code>var</code>。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602746808280-6743bf6c-34b5-402a-abe6-397eb76c5380.png" alt=""></p>
<p>注意细节，如果是变量值是字符串，要用双引号把它包起来：</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>Postman内置了全局变量，输入 <code>{{</code> 会出现提示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1700621852579-0ae851ef-9214-4880-8eb2-2a0cb06c63c5.png" alt=""></p>
<p>使用示例：</p>
<div class="language-json line-numbers-mode" data-ext="json"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>环境变量还可以在测试用例里去修改值：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694424229773-4c83c6cf-0640-4c8a-aa33-38097d84f0cd.png" alt=""></p>
<h3> 请求设置</h3>
<p>对于请求体的发送，一般进行如下设置：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602747761421-f2c415ec-1b1c-48f6-976d-fbc958fdcbf3.png" alt=""></p>
<p>还可以利用 Pre-request Script， 在请求前动态修改请求设置。<br>
比如有以下场景：</p>
<ul>
<li>线上环境接口路径为：/app-name/api/v1/api-name</li>
<li>本地环境接口路径为：/v1/api-name，也即线上环境接口地址多了两个前缀</li>
</ul>
<p>观察请求在 Postman 中的数据结构如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695779354530-f92268b0-1294-4ff7-b9d0-7a4cdd48bb4d.png" alt=""><br>
则可以编写前置脚本如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695779446073-c53065f9-07a8-4fd5-a062-390282dfd91e.png" alt=""></p>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h2> 接口测试</h2>
<h3> 编写用例</h3>
<p>在 Tests 标签页里，即可编写测试，在 SNIPPETS 里会有相应的示例。</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602748404916-daf8d633-1a35-48f8-a652-cc13c16244b5.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>下面给出常用CRUD相关接口的测试用例代码：</p>
<ul>
<li>设置 jwt</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>设置当天时间：</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><ul>
<li>创建</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>修改</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>查询</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>删除</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><ul>
<li>确保返回的数据里没有特定的数据</li>
</ul>
<div class="language-javascript line-numbers-mode" data-ext="js"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><h3> 上传文件</h3>
<p>如果要上传文件，则一般进行如下设置：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602747847485-5d86f2f6-18cc-4262-b5fd-69349feb4bbe.png" alt=""></p>
<p>注意要把文件放到工作目录中。查看工作目录的方法如下：</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1696845111698-7494d4ca-4a10-401b-9907-b93b1b2f3474.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1696845104321-e3956555-f26c-4c57-9d78-d14571a25dd6.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>把要上传的文件放到该目录下：</p>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1696845627177-ede00f46-b2fb-48d8-9f23-eecd342901ea.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>这样才方便后续运行集合、以及持续集成。</p>
<h3> 运行集合</h3>
<p>本地调试好了，把代码部署到线上环境后，就可以使用 postman 对线上的接口进行测试了。</p>
<p>先切换到对应的环境，再重置变量当前值<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1603339930271-36e9f9e5-8013-40fa-9fac-e7cb642ba451.png" alt=""></p>
<p>再选择集合，点击 Run collection:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695864657881-dc204c38-386e-47d2-a377-083397bacad1.png" alt=""></p>
<p>点击运行，可以看到集合内所有接口的执行结果：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1602748206140-245e5362-ffe3-4d2f-b3a3-61f781730e86.png" alt=""></p>
<p>提示：如果单独调试成功，运行集合却失败，注意检查文件是否已保存。如果文件处于编辑未保存状态，运行集合时使用的是修改前的接口。</p>
<h2> 持续集成</h2>
<h3> 导出接口</h3>
<p>导出接口合集：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694594443532-5ca5008a-4e57-4eb1-909d-2b40ff4241ff.png" alt=""></p>
<p>使用推荐的格式，导出一个 json 文件。</p>
<h3> 导出环境变量</h3>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695779153775-1cc9a182-a8e2-4b52-baf5-34e9c3173dc3.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1695779160975-7a625a91-bd51-45c5-b095-d89529b8ef51.png" alt="" tabindex="0"><figcaption></figcaption></figure>
<p>也是导出一个 json 文件。</p>
<h3> 复制要上传的文件</h3>
<p>把要上传的文件，复制到项目根目录。</p>
<p>如果没有上传文件的接口，忽略此步骤。</p>
<h3> 提交到Git</h3>
<p>把导出的 json 文件放到项目根目录中（与上传文件同级），并提交到 Git<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694594578211-b381d377-f6a5-4dd2-be82-0c9295f10fc6.png" alt=""></p>
<h3> 建立CI任务</h3>
<p>以 Gitlab 为例，修改<code>.gitlab-ci.yml</code>，增加以下内容：</p>
<div class="language-java line-numbers-mode" data-ext="java"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>推送代码，即可看到流水线<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694594759551-cb5a8564-cf20-4491-9378-3b6408e2c9fc.png" alt=""></p>
<p>结果如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694594789278-08b7d7e5-397b-47a9-9cbe-2f989a357ae1.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1694596433278-eb3e5566-0126-4e93-9dfe-a9c4d8edae51.png" type="image/png"/>
    </item>
    <item>
      <title>使用 pytest 为LLM应用添加回归测试</title>
      <link>https://levy.vip/software-testing/use-pytest-for-regression-testing-in-llm-app.html</link>
      <guid>https://levy.vip/software-testing/use-pytest-for-regression-testing-in-llm-app.html</guid>
      <source url="https://levy.vip/rss.xml">使用 pytest 为LLM应用添加回归测试</source>
      <description>使用 pytest 为LLM应用添加回归测试 回归测试的必要性 基于 LLM 的 Chat 应用大量依赖了 Prompt Engineering，而用户的输入又千奇百怪，调整了 Prompt 模板，很可能会有意想不到的效果：满足了新需求，却破坏了旧功能。 因此，LLM应用比任何时候都需要回归测试，确保在迭代过程中，不破坏旧功能、不让已修复的bug复现。 而回归测试，当然是自动化执行效率才高。本文交分享如何使用 pytest 对 LLM 应用进行自动化的回归测试。</description>
      <pubDate>Fri, 08 Dec 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 使用 pytest 为LLM应用添加回归测试</h1>
<h2> 回归测试的必要性</h2>
<p>基于 LLM 的 Chat 应用大量依赖了 Prompt Engineering，而用户的输入又千奇百怪，调整了 Prompt 模板，很可能会有意想不到的效果：满足了新需求，却破坏了旧功能。</p>
<p>因此，LLM应用比任何时候都需要回归测试，确保在迭代过程中，不破坏旧功能、不让已修复的bug复现。</p>
<p>而回归测试，当然是自动化执行效率才高。本文交分享如何使用 pytest 对 LLM 应用进行自动化的回归测试。</p>
<!-- more -->
<h2> pytest</h2>
<h3> 安装</h3>
<p>pytest 的安装就有坑，如果是使用虚拟环境 <code>venv</code>，安装姿势不正确的话，就会在执行测试用例的时候报错：<code>ModuleNotFoundError: No module named xxx</code>，具体原因参考<a href="https://medium.com/@dirk.avery/pytest-modulenotfounderror-no-module-named-requests-a770e6926ac5" target="_blank" rel="noopener noreferrer">这篇文章</a>。</p>
<p>正确的安装步骤：</p>
<ol>
<li>新开一个 bash 终端</li>
<li>pip uninstall pytest # 删除全局的 pytest</li>
<li>cd xxx &amp;&amp; source ./venv/Scripts/activate # 激活虚拟环境</li>
<li>pip install pytest # 在虚拟环境中安装 pytest</li>
<li>pytest # 启动测试</li>
</ol>
<h3> 配置</h3>
<p>在项目根目录新建 pytest.ini 文件，最简单的配置如下：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>更多的配置可参考<a href="https://docs.pytest.org/en/stable/reference/customize.html" target="_blank" rel="noopener noreferrer">文档</a>。</p>
<h3> 用例</h3>
<p>pytest 会自动收集测试用例，要求用例满足以下规范：</p>
<ol>
<li>文件名以 test_ 开头，如：test_intention.py</li>
<li>用例名以 test_ 开头，如：def test_my_method():</li>
</ol>
<p>为避免加载不到自定义的函数，需要包含以下代码：</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>给个实际的例子：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>在终端激活虚拟环境后，执行 <code>pytest</code>即可运行上述用例。</p>
<p>如果只想执行单个文件，则指定文件名即可：</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>如果想出错马上退出，带 -x 参数即可：</p>
<div class="language-python line-numbers-mode" data-ext="py"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><h2> 持续集成</h2>
<p>经过上述的步骤，我们可以在本地对自己的改动进行自动化的回归测试，但这还不够——因为有可能别人修改了代码，却不进行自测！</p>
<p>所以，我们还需要借助 CI 工具，在有人往代码仓库中提交改动后，立刻执行一次回归测试。</p>
<p>以 Gitlab CI 为例（点击查看<a href="https://levy.vip/git/gitlab-ci.html" target="_blank" rel="noopener noreferrer">安装教程</a>），<code>.gitlab-ci.yml</code> 文件如下：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>提交代码后就会触发自动化测试、构建镜像并推送。效果截图如下：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702014482966-9391b0d1-906b-4c23-b74b-41c1a2dcc305.png" alt=""><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702014525088-10240012-bad2-4b94-a06d-154adb3f1186.png" alt=""></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/md-image-kit/1702014482966-9391b0d1-906b-4c23-b74b-41c1a2dcc305.png" type="image/png"/>
    </item>
    <item>
      <title>科学上网</title>
      <link>https://levy.vip/tools/how-to-connect-to-internet.html</link>
      <guid>https://levy.vip/tools/how-to-connect-to-internet.html</guid>
      <source url="https://levy.vip/rss.xml">科学上网</source>
      <description>科学上网 说明 AI时代，学会正确上网是必备的技能。不然，谷歌用不了你还能忍，但 New Bing 跟 ChatGPT 都用不了，你还能忍？ 本文讲教大家如何购买稳定的包年上网套餐，为使用各种 AI 工具打下基础。 购买指南 点击进入服务页面, 看到如下页面：</description>
      <pubDate>Mon, 20 Jun 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<h1> 科学上网</h1>
<h2> 说明</h2>
<p>AI时代，学会正确上网是必备的技能。不然，谷歌用不了你还能忍，但 New Bing 跟 ChatGPT 都用不了，你还能忍？</p>
<p>本文讲教大家如何购买稳定的包年上网套餐，为使用各种 AI 工具打下基础。</p>
<h2> 购买指南</h2>
<p><a href="https://cp.cloudnx.cc/aff.php?aff=22930" target="_blank" rel="noopener noreferrer">点击进入服务页面</a>, 看到如下页面：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172119852.png" alt="image.png"></p>
<p>点击注册账户, 会出现如图提示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172124456.png" alt="image.png"></p>
<p>点击购买产品，就会出现查看产品列表面（为什么要这么绕，因为这是保护措施，避免产品主页被黑）。</p>
<p>推荐购买 Basic 套餐，一个人用的话，学习工作、娱乐看视频，每月 50GB 足够了。多个客户端可以同时在线，反正就是随便玩！算下来，一天不到7毛钱，很划算了。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/img.png" alt="image.png"></p>
<p>点击购买后，选择包年，即可享受优惠价格，如下图所示：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/img_1.png" alt="image.png"></p>
<p>支持支付宝，购买非常方便。</p>
<h2> 客户端</h2>
<p>支持全平台客户端：</p>
<ul>
<li>Mac 推荐 <a href="https://github.com/yichengchen/clashX/releases" target="_blank" rel="noopener noreferrer">ClashX</a></li>
<li>Windows 推荐 <a href="https://github.com/shadowsocks/shadowsocks-windows/releases" target="_blank" rel="noopener noreferrer">Shadowsocks</a></li>
<li>Android 推荐 V2ray</li>
<li>iOS 推荐 Shadowrocket（花点小钱，使用美区 apple id——文末有分享如何申请）</li>
</ul>
<h2> 导入配置</h2>
<p>以 Shadowsocks 为例。</p>
<p>根据客户端复制相应的订阅地址:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172131049.png" alt="image.png"></p>
<p>先禁用系统代理:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172136278.png" alt="image.png"></p>
<p>点击在线配置:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172141931.png" alt="image.png"></p>
<p>输入URL，点击更新:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172145950.png" alt="image.png"></p>
<p>选择一个服务器:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172149930.png" alt="image.png"><br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172155098.png" alt="image.png"></p>
<p>再恢复系统代理，选择PAC模式:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172160264.png" alt="image.png"></p>
<p>就可以愉快地上网啦！</p>
<h2> 其他</h2>
<h3> 500 内部代理错误</h3>
<figure><img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172164184.png" alt="image.png" tabindex="0"><figcaption>image.png</figcaption></figure>
<p>出现此问题时，一般是内网自定义域名不允许走代理，需要禁用掉系统代理:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172170331.png" alt="image.png"></p>
<h3> 修改PAC文件</h3>
<p>PAC模式是指：根据规则识别某网站是否需要使用代理访问。</p>
<p>什么时候需要修改PAC文件呢？</p>
<ul>
<li>当某个网站不想走代理</li>
<li>设置某网站一定走代理</li>
</ul>
<p>操作如下（以Shadowsocks为例）:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172175096.png" alt=""></p>
<p>按下图所示，模仿添加，即可实现遇到下列网站时选择直连:<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172179825.png" alt=""></p>
<p>不走代理的示例修改:</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>必走代理的示例修改:</p>
<div class="language-bash line-numbers-mode" data-ext="sh"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div><div class="line-number"></div></div></div><p>保存后记得重启软件。</p>
<h3> 使用 New Bing</h3>
<p>再给一个配置 Clash 使用 New Bing 的示例：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172184433.png" alt="image.png"><br>
编辑文件：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172189506.png" alt="image.png"><br>
添加如下设置：</p>
<div class="language-yaml line-numbers-mode" data-ext="yml"><div class="line-numbers" aria-hidden="true"><div class="line-number"></div></div></div><p>效果如下图：<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172193803.png" alt="image.png"><br>
之后再重新加载配置，即可打开 New Bing 页面。</p>
<p>但要让 New Bing 回答问题，还要设置全局模式，并选择正确的节点，如图所示。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172198850.png" alt="image.png"></p>
<h3> 申请美区apple id</h3>
<p>在官网创建申请: <a href="https://appleid.apple.com/account" target="_blank" rel="noopener noreferrer">https://appleid.apple.com/account</a></p>
<p>不要用 qq邮箱, 注册不会的成功的。</p>
<p>需要注意的是，地区请选择：Alaska，否则后续充值的话要交税😅</p>
<p>相关的地址信息，可以使用<a href="https://www.prepostseo.com/tool/fake-address-generator" target="_blank" rel="noopener noreferrer">美国地址生成器</a>，按如图所示选择，再点击生成即可获得注册时的必要信息。这样就不需要输入信用卡信息。<br>
<img src="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172198851.png" alt="image.png"></p>
]]></content:encoded>
      <enclosure url="https://raw.gitmirror.com/levy9527/image-holder/main/docs/tools/1682172119852.png" type="image/png"/>
    </item>
  </channel>
</rss>