<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>且听书吟</title>
        <link>https://yufan.me</link>
        <description>诗与梦想的远方</description>
        <lastBuildDate>Wed, 06 May 2026 05:41:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>WordPress 3.2.1</generator>
        <language>zh-CN</language>
        <image>
            <title>且听书吟</title>
            <url>https://yufan.me/logo.svg</url>
            <link>https://yufan.me</link>
        </image>
        <copyright>All rights reserved 2011, 雨帆</copyright>
        <category>文章</category>
        <category>杂谈</category>
        <category>杂思</category>
        <category>编程</category>
        <category>笔记</category>
        <category>小说</category>
        <atom:link href="https://yufan.me/cats/coding/feed" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[《Go 语言设计与实现》书评]]></title>
            <link>https://yufan.me/posts/book-review-in-go</link>
            <guid isPermaLink="false">https://yufan.me/posts/book-review-in-go</guid>
            <pubDate>Wed, 01 Oct 2025 12:57:19 GMT</pubDate>
            <description><![CDATA[我觉得这本书更像是写给作者自己看的。如果想要让更多人看懂，或许还需要更多的努力。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2025/10/2025100121055700.jpg"><img src="https://cat.yufan.me/images/2025/10/2025100121055700.jpg?v=1777738726110" alt="Go 语言设计与实现" width="1200" height="696" data-thumbhash="afgFBIBIB2d6h3Z1coXQpH1IBw==">
<p>第一次接触左老师的博客《面向信仰编程》应该是在 2020 年，那时候我在腾讯云工作，内部要求统一转 Go。作为一个 Java，在寻找学习资料时，我找到了左老师的博客，被里面关于 Go 语言的细节文章和插图惊艳到。后来在和熟悉的图灵编辑英子老师聊天时，才知道她正好是左老师的《Go 语言设计与实现》的编辑。于是，在书出版的第一时间，我就拿到了对应的样书。</p>
<p>转眼已经过去四年，上周一我还特意重新阅读。我觉得是时候认真地点评一下这本书了。</p>
<p>翻开这本书的豆瓣评价，可以发现它有着极大的两极化反应。喜欢的人如饮甘露，赞不绝口；不喜欢的人则将语言抬升到极端，批评得非常尖锐。</p>
<p>我个人印象最深的是，编辑老师曾提到左老师的语言过于简洁。简洁的语言在写博客时是可以接受的，因为博文一般较短，通常只有千字左右，更多的是讲重点。而写书的要求则完全不同，给出版社写书的朋友可能知道，最初拟定出版合同时，图书的大纲和选题已经经过内部评审，且每个阶段要交的稿件基本上已经确定。因此，在写书之初，就需要有一个清晰的框架，而不是想到哪里写到哪里。</p>
<p>框架（目录）确定之后，接下来就是在框架上填充内容，明确每个章节的重点。这实际上是语言组织与表达的艺术。当然，编辑在此过程中会帮忙审定和优化字句，但写作的核心始终是作者本身，编辑更多是对文字的二次加工。</p>
<p>《Go 语言设计与实现》的首要问题，便是写作时的表达问题，尤其是在已有框架的基础上。例如，第一章讲解调试源代码，第二章讲解编译原理。两章共计约 50 页的内容，涵盖了大学本科一学期编译原理的课程内容。值得一提的是，许多读者并非计算机专业出身，缺乏系统学习相关课程的背景。即使是有相关基础的读者，作者在组织内容时也存在一定的失衡。例如，在讲解词法分析时提到的 Lex，我相信大多数读者并不理解。作者自己也提到，从 Go 1.5 开始，Go 编译器不再依赖 C 的 Lex。不过，这只是干巴巴地附上一句，而没有深入探讨，甚至连自举的内容都懒得提一下。再到文法分析部分，涉及到的 LL、LA、LR 等概念，估计很多读者都会一头雾水。与之对比，ANTLR4 的作者在其类似的书中，通过清晰的图示和流程图将闭包、DFA 等概念呈现得一目了然，读者很容易就能理解。</p>
<p>另外，书中的代码片段也值得一提，甚至可以说是灾难。代码应该服务于书本内容，但左老师的做法似乎是讲到哪就贴上相应的代码。正确的做法是，首先讲解宏观的设计思想和代码的整体结构，然后再逐步细化到具体的代码，解释每一行代码的作用。与雨痕和郑建勋老师的书籍相比，左老师在这方面做得稍显不足。</p>
<p>起初，插图是我对左老师的博客非常赞叹的地方，甚至研读过他关于如何绘制插图的博文。但在书中，虽然插图依然精美，但并不总是恰到好处。很多时候，插图的色彩堆砌反而让人分心，读者的注意力容易集中在色彩选择上，而不是图示本身的内容。例如，为什么有些部分是红色的，为什么有些部分是绿色的？实际研究后发现，这些配色并非为了区分主次，只是为了视觉效果而已。</p>
<p>说到缺点，我就提到这里。整体而言，这本书并不是一本差书，否则我也不会重温它。只可惜，左老师本可以做得更好。他在 Go 语言的知识体系和研究深度方面，远远超过了我这个门外汉。很多时候，我们对一本书的评价，可能轻轻松松地说一句“好”或“不好”。但实际上，写一本书并不是一件简单的事情。只不过，我觉得这本书更像是写给作者自己看的。如果想要让更多人看懂，或许还需要更多的努力。</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/book-review">书评</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/book-review-in-go.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[一道无趣的面试编程题]]></title>
            <link>https://yufan.me/posts/algo-find-the-lowest-costs</link>
            <guid isPermaLink="false">https://yufan.me/posts/algo-find-the-lowest-costs</guid>
            <pubDate>Sat, 13 Apr 2024 07:42:12 GMT</pubDate>
            <description><![CDATA[最近经济大环境依旧没能从疫情中走出来，身边有不少小伙伴被裁员或者是公司倒闭失业。好友群里讨论最多的话题就是面试，自然少不了讨论面试题。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024041513050511.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step1.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step2.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step3.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step4.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step5.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step6.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024041510373084.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step7.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/algo-minimal-costs/step8.svg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2024/06/2024062101010412.jpg"><img src="https://cat.yufan.me/images/2024/04/2024041513050511.jpg?v=1777738726110" alt="羨望" width="928" height="720" data-thumbhash="lvcNDYJ4aHiPh4eIiIeHiQVyUGEI">
<p>最近经济大环境依旧没能从疫情中走出来，身边有不少小伙伴被裁员或者是公司倒闭失业。好友群里讨论最多的话题就是面试，自然少不了讨论面试题。昨天一位相识多年的好友向我求助，他当时正好在面试，需要现场编程。</p>
<p>当时刚好不忙就看了一下题目，感觉很无趣，但还是耐着性子文字给他讲了讲，顺带着画了张简图，可是他还是没懂。原题如下：</p>
<blockquote>
<p>一个城市可以近似看成 n * m 的网格图，A 公司有 k 个维修点，每个维修点有固定的坐标，城市里面有 h 个客户需要修理手机，客户有固定的坐标。维修员在地图上只能上下左右走，不能斜着走，每走一个格子需要 2 块钱的花费。每个维修点拥有无数个员工，每个员工可以被派去为一个客户服务。城市里面有 z 个地方在修理管道，这些地方是不能走的。可能有一些客户是被隔离的（上下左右都在修管道），这里是不需要派员工去修理手机了。A 公司为了节省财力，想找到最小的花费。</p>
<p><strong>输入</strong>：</p>
<p>第一行给出两个正整数 n, m （0 &lt; n &lt; 1000, 0 &lt; m &lt; 1000）。<br><!-- -->第二行给出 k（0 &lt; k &lt; 20）以及 k 个维修点的坐标。<br><!-- -->第三行给出 z（0 &lt; z &lt; 100）以及 z 个坐标。<br><!-- -->第四行给出 h（O &lt; h &lt; 100）以及 h 个坐标。<br><!-- -->保证客户，维修点以及修理管道都在 n * m 的地图里面。</p>
<p><strong>输出</strong>：最小的花费。</p>
</blockquote>
<p>样例</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">输入样例</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">411223344</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 99</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 99</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 88</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 88</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7777</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">输出样例</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1008</span></span></code></pre>
<p>这道题乍一看，看起来很唬人字很多，又是还有拦路虎，要找最短路径啥的，但其实是一道阅读理解题。一般现场编程面试，主要看你现场的反应和理解力，算法或者数据结构的东西，反而涉及不会太多。</p>
<p>这也使得这道题在弄懂原理后相当无趣，但考虑我这朋友确实经验尚浅，所以我还是给他继续讲下去，顺带着给了代码实现。这篇博客便是当时内容的摘录整理。</p>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step1.svg?v=1777738726110" alt="Step 1" width="400" height="400" data-thumbhash="1faBBQAA/RebeCMD1AJ2XrT7b4eYeDB2dQ==">
<center><p>做任何算法题，第一步是理解题意，第二步是设想最简单的情况，再慢慢推导到复杂情况。首先，我们先不考虑存在阻塞的情况。最简单场景里，顾客和维修点在一个
1 x 1 的格子的一条边上，这个时候他们间的最短距离为 1。</p></center>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step2.svg?v=1777738726110" alt="Step 2" width="400" height="400" data-thumbhash="1VeCBQAAvBSriTMH0wSarPiLTlOZaEBJdA==">
<center>然后我们更进一步，如果他们在一个格子的对角线上呢？他们间的最短路径有两条，为 2。</center>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step3.svg?v=1777738726110" alt="Step 3" width="400" height="400" data-thumbhash="F0eCBQAAmEl6logIZ/xR2E9WT2d4dVA5Fw==">
<center><p>结合初中的几何学知识，我们首先知道一个基本知识，两点之间，直线最短。所以，维修点和顾客在同一条直线上时，他们之间的距离就是直线距离。</p></center>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step4.svg?v=1777738726110" alt="Step 4" width="800" height="400" data-thumbhash="0QeCA4AAW2udmFCZq7/3eehHkIhnRic=">
<center>然后我们再稍微复杂一点，此时顾客和维修点之间是田字格，最短路径就有三条，距离为 3。</center>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step5.svg?v=1777738726110" alt="Step 5" width="800" height="400" data-thumbhash="V+eBA4AAJk+MmJDsif9t23Mfg5eiWjg=">
<center><p>等到田字格的时候，相信聪明的你已经发现了规律。那就是顾客到维修点的最短距离，等于他们所形成的矩形的横纵两条边边长的总和。按照上面右侧图片所示的箭头所行走的距离都等于这个最短路径。</p></center>
<p>一般情况下，面试场景的编码题已经可以开始写了。对应的编程思路就是，从维修点出发，在与顾客构成的矩形边界里面，不断逼近，只要能走通那么我们之间就有了最短距离。再把不同维修点到顾客的最短距离排序，选出最小的距离来进行计算费用。</p>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step6.svg?v=1777738726110" alt="Step 6" width="800" height="400" data-thumbhash="GFiCA4AAuwecycF1sl8Y+3AtxHdnOTk=">
<p>倘若以上面的推论作为最终编码的方式，虽然不能说完全错误，但是在当下这个面试很卷的时代，还是有可能被 PASS，为什么呢？因为我们还没有引入阻塞的概念。我们随便画两种阻塞的情况，并且假定这里都属于在当时条件下的最短路径，那么阁下又该如何应对？😆</p>
<img src="https://cat.yufan.me/images/2024/04/2024041510373084.jpg?v=1777738726110" alt="头号玩家 电影截图" width="813" height="484" data-thumbhash="UBgKBIInp5l/doeId3kBjDCCBw==">
<p>某种意义上说，我们的确需要从头来审视这道题目。从前面的分析和题目中，我们得出两个结论。</p>
<ol>
<li>最短的距离永远是尽量在水平和垂直距离上向目标靠近的走法。</li>
<li>用户每次前进，在没有阻塞的时候，其实可以最多可以往四个方向去走。</li>
</ol>
<p>以此为基础，我们就可以稍微来复习一下大学的算法知识了，贪心算法（贪婪算法）。贪心算法的定义网上随随便便都能找到，这里就不再复述，我们更多地是需要去思考在这个场景的贪心算法如何使用。</p>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step7.svg?v=1777738726110" alt="Step 7" width="800" height="400" data-thumbhash="G+iBA4AAVjp7d4B0k68I+qMHkYiDd1c=">
<p>贪心算法的第一步，就是找寻从顾客开始，所有可能能行走方向距离为 1 的点有哪些（图中蓝色的点）。接着，我们可以以这些距离为 1 的点为基础，去找寻所有距离为 2 的点（图中绿色的点）。以此类推，直到所有的点都没有下一个可以行走的点了。而每计算一次距离为 N 的点的时候，都可以尝试看看里面是否有对应的维修点，如果有，那么终止检索，这个 N 便是最短距离。</p>
<img src="https://cat.yufan.me/images/recaps/algo-minimal-costs/step8.svg?v=1777738726110" alt="Step 8" width="800" height="400" data-thumbhash="oemBA4AAWEl9h4BKem8X+cUEkpeAhgY=">
<p>如上图所示，在我们查找距离为 4 的点的时候，我们就能找到目标维修店，那么我们可以认定，起最短距离就是 4。</p>
<p>下面就可以考虑编码了，倘若是在算法竞赛里面（这种题连算竞入门题都不算啦），首先需要考虑的是时空效率。我们首先定义一个二维数组，并在上面放上维修店，假定魔力数字 -1。然后放上所有阻塞的点，假定魔力数字为 -2。数组里面数字为 0 的地方代表没有走过的点，为 1 的值则代表走过的点。</p>
<p>那么此检索最短路径的算法大概应该类似如下内容，类伪代码，不代表最终能运行品质：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[][] routines </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[x][y];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> record</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> x, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> y) {}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> record</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SearchResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">boolean</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> found, List</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Point</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> next) {}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> int</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> findMinimalRoutine</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[][] routines, Point customer) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    List&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt; next </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Collections.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">singleton</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(customer);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> minimalPath </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        result </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> findNextPoints</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, next);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (result.found) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> minimalPath;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        minimalPath </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        next </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result.next;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">while</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (next </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">next.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isEmpty</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> SearchResult </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">findNextPoints</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[][] routines, List</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Point</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> currentPoints) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    List&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt; resultPoints </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ArraryList&lt;&gt;();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Point currentPoint </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> currentPoints) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        List&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt; nextPoints </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> findNextPoints</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, currentPoint);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Point nextPoint </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nextPoints) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (routines[nextPoint.x][nextPoint.y] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SearchResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, Collections.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">emptyList</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            routines[nextPoint.x][nextPoint.y] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        resultPoints.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(nextPoints);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SearchResult</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, resultPoints);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> List</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Point</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> findNextPoints</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[][] routines, Point point) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    List&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt; nextPoints </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ArraryList&lt;&gt;(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">availablePoint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, point.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, point.y)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nextPoints.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(point.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, point.y));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">availablePoint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, point.x, point.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nextPoints.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(point.x, point.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">availablePoint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, point.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, point.y)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nextPoints.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(point.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, point.y));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">availablePoint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(routines, point.x, point.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nextPoints.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Point</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(point.x, point.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nextPoints;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> boolean</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> availablePoint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[][] routines, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> x, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> y) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> routines.length </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> routines[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].length </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (routines[x][y] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> routines[x][y] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<img src="https://cat.yufan.me/images/2024/06/2024062101010412.jpg?v=1777738726110" alt="Fin" width="1280" height="734" data-thumbhash="DQgGDID6+bd4hnV+d4f5eZifdw==">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/algorithm">算法</category>
            <category domain="https://yufan.me/tags/interview">面试</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/algo-find-the-lowest-costs.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[弃用 WordPress 了，但我相当“后悔”]]></title>
            <link>https://yufan.me/posts/switch-blog-to-nextjs</link>
            <guid isPermaLink="false">https://yufan.me/posts/switch-blog-to-nextjs</guid>
            <pubDate>Sun, 07 Apr 2024 08:09:09 GMT</pubDate>
            <description><![CDATA[如你所见，当你访问这篇文章的时候，我已经把写了 13 年的博客程序从 WordPress 迁移到了自己用 Next.js 写的程序，这可能是我第 N 次尝试使用其他的方式写博客，但我想绝对不会是最后一次。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024040719182344.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024040719297778.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024040719182210.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/next.js.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/contentlayer.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/velite.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/replace-artalk.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro-actions.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro-uploader.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/opendal.png"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/open-graph.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/zeabur-astro.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024040719244778.jpg"><img src="https://cat.yufan.me/images/2024/04/2024040719182344.jpg?v=1777738726110" alt="あんよ - crossroads" width="3000" height="1688" data-thumbhash="mMYJFIJBV3iAenl7d3fbotAeCw==">
<div class="tw:max-w-87.5 tw:mt-5 tw:mb-5.5 tw:max-xl:mx-auto tw:max-md:max-w-full tw:max-md:mt-0 tw:max-md:mx-0 tw:max-md:mb-5"><div class="aplayer" data-id="28468918"></div></div>
<p>如你所见，当你访问这篇文章的时候，我已经把写了 13 年的博客程序从 WordPress 迁移到了自己用 Next.js 写的程序。这可能是我第 N 次尝试使用其他的方式写博客，但我想绝对不会是最后一次。</p>
<p>第一次接触类博客的平台还是 20 年前，初一同学在课间说道：“我开通了 QQ 空间，快去给我踩一踩。”正儿八经地写作，还要追溯到 15 年前，那时的我是那么多愁善感，什么东西都想要“为赋新词强说愁”。我的足迹遍布各类 SNS 平台，在上面书写各种现在看来相当幼稚的话语。</p>
<p>因为专业的原因，在大学期间接触到了独立博客和 WordPress，当时的我就被这种独立而又个性的写作方式惊到了。那时还属于独立博客的黄金年代，QQ 雷锋群里群英荟萃，我那孱弱的前端知识就是通过此类途径习得。而我没想到的是，<a href="/posts/the-beginning-of-blog/">当初的开博宣言</a>竟然一写就是 13 年。</p>
<p>年龄真是个好东西，因为其增长，我们能不断地丰富自己的阅历，也能见证所谓的“历史”。兜兜转转，写博客的人来来去去。有些相识数载至今还是好友，有些已经消失再无音讯。而我，对这 WordPress 也是又爱又恨，这期间换过太多的平台。有简单的 Hexo、Hugo，也有同样基于 LAMP 平台的 Typecho、Emlog、Z-Blog，还有之前很时髦的 Ghost。很多文章评论都放在了多说、Disqus 上，因为这来来回回的“搬迁”遗失大半。到头来，竟然还是这 WordPress 最好用，如同家里的“黄脸婆”，阅尽世间百态，才发现老婆是真爱。虽然至今还是不会写 WordPress 主题，但好在漂亮的主题数不胜数，凭着钞能力也能基本解决。</p>
<p>但，我还不甘心。</p>
<p>这十几年来前端风起云涌，每天都有新的东西诞生。13 年前博客用个 jQuery 就很了不起了，很多页面还使用表格布局，为了 IE6 要写一堆 Hack，那个时候声称不兼容 IE 的网站都属于“珍惜动物”。而如今 IE 早已不复存在，继任者 Edge 也把自己变成了 Chrome 的模样。前端相关的框架更是层出不穷，光打包工具我就完完整整地经历了 Grunt、Gulp、Webpack、Rollup、Vite、SWC 等，更别提其他的了。在这些技术的背景下，我这基于 jQuery 的 WordPress 就显得稍微有点 old school 了。</p>
<p>上周和 Tison 聊天，发现他正好准备基于 Astro 弄个<a href="https://github.com/tisonkun/dacapo" rel="nofollow" target="_blank">新的博客</a>。沉眠于心中多年的想法终于浮起，既然用了这么多博客平台都不满意，为何不能自己写一个。Next.js 这么时髦，其 App Router 还能用最新的 React Server Component，我都眼馋很久了。于是说干就干，趁着正好清明放假，我把几乎所有的时间都投入在了这一“浩大”的造博运动中。</p>
<h2 id="logo-设计">Logo 设计<a href="#logo-设计"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/2024/04/2024040719297778.jpg?v=1777738726110" alt="Blog Logo" width="3334" height="2158" data-thumbhash="9/cJDYD0R2d3eIeLdUeXifiZq6+6">
<p>对我而言，技术牛逼与否并不是最重要的，最重要的是颜值。如果不好看，那就直接 pass。所以还没写博客，第一步，我就在考虑重新设计自己的博客 logo。对于之前用的 logo，其实问题并不大，主要的问题是矢量化。绘制这个 logo 的时候还是 6 年前，像素被限制在了 65px，在 PSD 底稿和对应字体都丢失的当下，想要放大适应视网膜屏幕等就显得有些困难。于是我借着这个机会，学习使用 <a href="https://sketch.com" rel="nofollow" target="_blank">sketch</a> 来绘制矢量图形。这当中最大的绘制难点就是 logo 中那两个不规则的半圆环，我画了两个圆去叠加，然后合并后一点点拖拽曲线。才调整到和以前相似的形状。</p>
<p>绘制“帆”字的时候已经稍微熟悉，就直接使用钢笔工具一个个锚点描边，再稍微调整曲线就好，但是没想到还是在和圆环合并上栽了大跟头。弄了半天才发现是路径曲线的闭合问题，这其中的曲折大概就像是以前在微博上看到的<a href="https://cat.yufan.me/images/2024/04/2024040719234718.jpg" rel="nofollow" target="_blank">这张长图</a>所描绘的景象。不过万幸的是，最终圆满地达成了我想要的效果。</p>
<p>在绘制 logo 的同时，我也一并绘制了黑白两种形态，为日后主题支持黑白模式准备，同时也绘制了 Github Poster 放在 <code>README.md</code> 上用于展示。这当中最有意思的事情，就是对以膨胀色与收缩色的概念的复习。以前自学设计的时候，觉得这东西很抽象。这次设计黑色 logo 的时候，发现明明是同样的大小，但是黑色的 logo 在屏幕上就是显得大上一圈。我不得不微调曲线，向内收缩一圈才在视觉的保持了 logo 的大小一致，相当有趣。</p>
<h2 id="字体使用">字体使用<a href="#字体使用"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/2024/04/2024040719182210.jpg?v=1777738726110" alt="OPPO Sans" width="2000" height="1125" data-thumbhash="DigBBIBseHavh3d1iWFtvyMJxA==">
<p>字体选择上分为博客正文字体和图片字体。以前并不懂版权，不知道字体很多是需要授权才能在一些公开媒介上使用的，这次特意选择了商业免费可用的字体。</p>
<p>正文字体使用的是 <a href="https://www.coloros.com/article/A00000050/" rel="nofollow" target="_blank">OPPO Sans 3.0</a>，5 年前在字谈字畅的 <a href="https://thetype.com/typechat/ep-113/" rel="nofollow" target="_blank">PodCasts</a> 得知此字体的公布，当即就喜欢上了。虽然和阿里普惠体、微软雅黑体等一众“汉仪兄弟们”师出同门，但是其变化是最讨喜的。它去掉了自冬青黑体时代就有的大喇叭口，字形更加方正，在笔画转弯处也更加刚正。其家族字体设计更多的是为传统非视网膜屏幕服务，所以夸张的笔锋搭配上抗锯齿就能有较好的观感。这种设计，放在当今连手机都是视网膜屏幕的年代，就显得很难看。所以 OPPO Sans 的设计，在当下很长一段时间，将成为我的首选字体。</p>
<p>Logo 部分的字体，主要是基于 <a href="https://booth.pm/ja/items/2347968" rel="nofollow" target="_blank">M+A1</a> 进行制作。虽然此字体是日本人制作，但是不同于在日本起绝对统治地位的明朝体，此字体是一款明朗的黑体，而且同时免费提供了多种粗细字型，简直感动到哭。最有意思的就是细款字形的笔画末端都有一定的隆起，结合之前学习到的<a href="https://www.thetype.com/2021/06/21723/" rel="nofollow" target="_blank">光陷阱</a>，想必此款字体在打印机下也能有极好的显示效果。</p>
<h2 id="技术选型">技术选型<a href="#技术选型"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/next.js.jpg?v=1777738726110" alt="Next.js" width="2800" height="1600" data-thumbhash="OggGBID3R6h7h3aLhnUAAAAAAA==">
<p>技术选型上这次有点激进，相比已经成熟很好用的 VitePress 等动静结合的生成工具，我义无反顾地选用了 Next.js 的 App Router，运行环境选用了更快的 Bun。至于其他方面，基本是照着 <a href="https://leerob.io" rel="nofollow" target="_blank">leerob</a> 的博客进行仿写。所以文章格式不再是常见的 Markdown，而是能插入动态内容的 MDX。对于历史的评论，我还是舍不得丢弃，毕竟有 3000 多条。所以再三对比之下，我选择使用 <a href="https://artalk.js.org" rel="nofollow" target="_blank">Artalk</a> 进行存储。有了多说的经历后，我也不敢使用任何第三方的 SaaS 服务存储评论，毕竟数据在自己手里才最安全。</p>
<p>博客以前最喜欢的音乐播放器是 Hermit-X，对应的作者荒野无灯已经神隐很久，获取网易音乐信息的 <a href="https://github.com/metowolf/Meting" rel="nofollow" target="_blank">Meting API</a> 也没持续更新。而 APlayer 更是不知道 <a href="https://diygod.cc" rel="nofollow" target="_blank">DIYgod</a> 什么时候能来“扫墓”，上次 APlayer 的诈尸还只是更新了个 License。所以在新博客环境只有 <a href="https://aplayer-react.js.org" rel="nofollow" target="_blank">Aplayer React</a> 这个名为 APlayer 实则是借着其样式用 React 完全重写的播放器可用，其在 RSC 下有点小问题，但估计短时间内作者也没时间修复。（谁叫我是前端菜鸟呢）</p>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/contentlayer.jpg?v=1777738726110" alt="ContentLayer" width="1280" height="640" data-thumbhash="DfcBFIIIp4h1iXd9jox4h3CH+A==">
<p>MDX 一开始是手动使用 <code>fs</code> 加载，使用 <code>grey-matter</code> 解析 <code>matter</code> 后再用 <code>next-mdx-remote</code> 渲染。但是鉴于自己太菜，很多页面的渲染都需要全量读取博文解析。后面改成了<a href="https://twitter.com/ilovek8s/status/1776809454790676827" rel="nofollow" target="_blank">推友推荐</a>的 <code>contentlayer</code> 先渲染静态化，再进行加载。但瞅着 <code>contentlayer</code> 这半死不活的状态，未来大概率还是要弃。😭</p>
<h2 id="update-20240414">Update 2024/04/14<a href="#update-20240414"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/velite.jpg?v=1777738726110" alt="ContentLayer" width="2886" height="1623" data-thumbhash="yvcBBII2DaRqt2aEaHD/seoN2g==">
<p>没错，一周后我又来屁颠屁颠地更新博客啦，一周过去后，我的博客也发生了不少变化。首先是搜索终于支持了，使用的是 fuse.js。前文提到的 Contentlayer 也被我废弃，换成了更好用的 Velite。</p>
<h2 id="update-20240522">Update 2024/05/22<a href="#update-20240522"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro.jpg?v=1777738726110" alt="ContentLayer" width="1200" height="630" data-thumbhash="jwcGDILzJ2lAiHdhl5wEygVMMw==">
<p>今天，我把博客从 Next.js 迁移到了 Astro，重写了部分代码的实现，优化了部分设计。Next.js 版本的博客，内容管理和生成工具使用的是 Velite.js，它的设计很有想法，作者也很有经验。但是对应的 Next.js 与其集成后，却给我带来了很多解决不了的问题。首当其中的就是 Next.js 默认是 SSR，我在 MDX 中生成的内容，部分需要 inline 到 Client 端使用 JS 去动态执行，结果无法实现。其次就是 Astro 先天的在内容管理与编写上下了不少功夫。基于上述理由，我花了两天多一点的时间，就轻松把博客用 Astro 重写了。</p>
<p>重写之后发现 Astro 其实还是比较不成熟，很多东西文档并不清楚，还是要看源码来理清其设计想法。比如 <code>Astro:content</code> 的设计原理和其使用的细节问题，再比如 <code>Astro:asset</code> 这个特定的 import.meta 的使用问题。整体上而言，很多细节问题其实是 Astro 变化太快，对应的文档还没有来得及讲清楚。但是最让我失望的是，Astro 的 SSR 并不完善，它的 inline CSS 和 JS 的实现，其实是靠 Vite 整合 Rollup 提供的能力。所以在 SSR 上，如果你在一个 Astro Component 里面 <code>is:inline</code> 去定义 JS，很有可能会遇到因为模块重用而导致的坑。</p>
<p>另一个我很失望的缺陷就是 Astro 对于 MDX 的支持问题。当前的版本对于 MDX，还是无法实现预渲染来全文输出 RSS。当然，我知道这并非 Astro 自己的问题，主要和 MDX 的复杂和生态息息相关。</p>
<p>迁移到 Astro 的同时，也将博客的 Comment 系统 Artalk 的数据库从 MySQL 切换成了 Postgres，对应的数据库查询相关的代码完全废弃重写，顺带着使用 DrizzleORM 来完成了相关的查询的编写。整体上的使用比想象中愉悦。</p>
<p><del>希望这是我最后一次重写博客吧。</del></p>
<h2 id="update-20240623">Update 2024/06/23<a href="#update-20240623"><span class="icon icon-link"></span></a></h2>
<p>截止上次更新，又过去了一个月。这期间博客经历了大大小小的数次优化，比较值得开心的就是在 Astro 4.11.0 的发版记录上榜上有名，修改了一处小 BUG。网站比较值得提及的重大变化有如下四点。</p>
<h3 id="astro-container-api-使用">Astro Container API 使用<a href="#astro-container-api-使用"><span class="icon icon-link"></span></a></h3>
<p>在一个月前我还在抱怨 Astro RSS 无法做到 MDX 渲染导致只能输出摘要，随后的 <a href="https://astro.build/blog/astro-490/" rel="nofollow" target="_blank">Astro 4.9</a> 就更新了 Container API 能自定义渲染某个 Astro 的页面模块。虽然 Container API 的设计更像是为了动态编程方式渲染某个具体的样式模块，包括但不限于 React，Astro，Vue 和 Solid 等。但因为能引入 MDX 的内容，所以就近似实现了输出 MDX 全文的效果。同时，因为 MDX 在渲染时可以替换对应的 Component 实现，所以在 RSS 中还能优化渲染结果。</p>
<p>Astro Container API 前前后后经过数次修改。在最新的 Astro 4.11 中可以说相对稳定下来，并且能在 Vercel 等 Serverless 平台部署不报错。如果有 RSS 全文输出类需求，可以考虑尝试。</p>
<h3 id="artalk-前端评论模块弃用">Artalk 前端评论模块弃用<a href="#artalk-前端评论模块弃用"><span class="icon icon-link"></span></a></h3>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/replace-artalk.jpg?v=1777738726110" alt="ContentLayer" width="1200" height="600" data-thumbhash="OggGFIKViqiKh3iqdX95UX8Y9g==">
<p>Artalk 作为后端评论系统，其实没啥问题，并且有相当不错的安全机制和设计，但是其前端样式怎么定义都不是很好看。早在准备自己实现评论系统的时候，第一个想法就是基于 Artalk 的 Rest API 改成 SSR 的模式加载评论。目前经过数次修改，其评论样式和效果已经和以前使用的 WordPress 基本接近。</p>
<p>在做评论系统的替换的同时，也在遗忘了很久的百度网盘的网站备份目录中找到了多说和 Disqus 的备份。经过修改和迁移，已经成功将多说的评论全部导入进 Artalk。而 Disqus 的备份，受限于欧盟的 GDPR 政策，已经不再提供评论者的 Email 信息，所以只能基于历史评论进行检索对比来确定用户信息进行导入。虽然已经尽了全力，但还是有部分评论被废弃。</p>
<h3 id="astro-actions-的使用">Astro Actions 的使用<a href="#astro-actions-的使用"><span class="icon icon-link"></span></a></h3>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro-actions.jpg?v=1777738726110" alt="ContentLayer" width="1200" height="600" data-thumbhash="OggGDIKGiqiJiIepZn95UW8Y9g==">
<p>因为博客的评论模块的替换，导入需要引入大量的 Rest 接口。结合以前有的喜欢按钮等接口，博客一共累计超过 4 个接口。以前都是散乱在 pages 目录里面，这次一并使用了 <a href="https://astro.build/blog/astro-480/" rel="nofollow" target="_blank">Astro 4.8</a> 的 Actions 进行统一定义和管理，整体感受如下：</p>
<ol>
<li>Actions 和 tRPC 的使用体验基本相似，都是 Typed 的。</li>
<li>生成的 actions 可以用 import 的方式在前端的 JS 里面调用，Vite 会打包整合。</li>
<li>使用 Actions 可以统一包装错误管理，调用上更加灵活。</li>
</ol>
<h3 id="astro-cdn-功能的整合和实现">Astro CDN 功能的整合和实现<a href="#astro-cdn-功能的整合和实现"><span class="icon icon-link"></span></a></h3>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/astro-uploader.jpg?v=1777738726110" alt="ContentLayer" width="1200" height="600" data-thumbhash="+/cFDIJnqpeJh3iGeH+Gf1H5Fg==">
<p>Astro 在 <a href="https://astro.build/blog/astro-220/" rel="nofollow" target="_blank">2.2</a> 引入了 CDN Support，其主要目的是对动态构建的 CSS、JS 和图片等内容，可以自定义它们的访问路径和 URL，Astro 会在最终构建的网站结果中予以替换对应的资源路径。但是 Astro 只实现前面说的内容，如果想要在生产环境能实现全套的 CDN 支持，还需要将构建出来的文件上传到对应的 CDN 服务提供商。如，上传到 UPYUN、七牛云、S3 等对象存储。</p>
<p>在这里我选用了用了近 10 年之久的 UPYUN，第一版是使用 0.25 引入的 <a href="https://astro.build/blog/astro-025/#new-astro-integrations" rel="nofollow" target="_blank">Astro Integration API</a> 来进行实现。Astro Integration API 是一个非常经典的观察者模式的设计，它对应了 Astro 构建的几个不同阶段，允许你定义不同阶段的特殊勾子，Astro 会在对应阶段调用你的自定义逻辑。对于我而言，我只需要在 <code>&#x27;astro:build:done&#x27;</code> 阶段读取所有想要上传的目录，进行上传即可。</p>
<p>这期间对上传逻辑进行了多次重构，第一版使用 UPYUN Node.js SDK，但是这个 SDK 年久失修，使用的还是有安全问题的 axios 版本。对于已经习惯上 fetch 一把梭的我，有点生理不适。考虑到很多云存储都支持了 S3 协议，并且使用 S3 协议能更灵活地切换到不同的存储服务。在第二版使用了 Client S3 来进行实现，这里面遇到的唯一问题就是 ContentType 需要显式设置，而原先的 UPYUN Rest API 能根据文件名自动设置。</p>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/opendal.png?v=1777738726110" alt="OpenDAL™" width="3932" height="2662" data-thumbhash="tcaFG4QrCEhceIsJa5ywxjg4Z3eAhyc=">
<p>第三版使用了基友 Tison 安利的 Apache OpenDAL™ 进行了重构，还是走的 S3 协议。Apache OpenDAL™ 是一个使用 Rust 编写的统一云存储层抽象接口，整体的代码质量和实现都很不错。但是 Node.js 部分的 Binding 使用 napi-rs 实现，对应的文档写得比较抽象，我是看 Rust 部分的文档反向推导 Node.js binding 该如何使用，期待文档的进一步完善。同时因为一些<a href="https://github.com/apache/opendal/issues/4782" rel="nofollow" target="_blank">已知问题</a>，暂时还无法直接在生产环境使用。</p>
<h2 id="update-20241016">Update 2024/10/16<a href="#update-20241016"><span class="icon icon-link"></span></a></h2>
<p>距离上次更新又过去了 4 个月，这期间看了不少书，博文倒是没写几篇。这期间博客的源码倒是没少折腾，但基本都是细枝末节的修改，如例行的升级依赖啥的，比较需要注意的更新有如下内容。</p>
<h3 id="补完全部文章的插图">补完全部文章的插图<a href="#补完全部文章的插图"><span class="icon icon-link"></span></a></h3>
<p>博客的文章书写时间已经有 15 年的跨度，这期间文章的很多插图，要么尺寸过小，要么丢失。在 3 年前切换回 WordPress 时进行了统一的梳理，将所有图片去除，转成了 Markdown 格式，但并未补全全部插图。这次借着切换为 Astro 的机会，将博客的文章都配上了精美的插图，并设置了对应的封面图片。（何等可怕的工作量）</p>
<h3 id="支持动态-og-图片">支持动态 OG 图片<a href="#支持动态-og-图片"><span class="icon icon-link"></span></a></h3>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/open-graph.jpg?v=1777738726110" alt="Open Graph" width="1200" height="768" data-thumbhash="zgcGHIR9d1aKeHhQeHeHYHEINg==">
<p>对于 OG 图片的支持，历时 3 个月才予以实现，最早的版本为选取文章的封面，后面改为使用 <a href="https://vercel.com/docs/functions/og-image-generation" rel="nofollow" target="_blank">@vercel/og</a> 生成。博客切换为 Astro 时一并放弃了 React Server Component 方案，对应的 OG 生成暂时搁置。在仔细学习了基于 napi 的 <code>@napi-rs/canvas</code> 后，本博客的 OG 图片使用 Canvas 绘制，并在构建的时候静态生成放于对象存储上来减少字体加载、渲染等耗时。目前 OG 的样式经过多次微调，暂时满意。</p>
<h3 id="使用-zeabur-serverless-function-部署">使用 Zeabur Serverless Function 部署<a href="#使用-zeabur-serverless-function-部署"><span class="icon icon-link"></span></a></h3>
<img src="https://cat.yufan.me/images/recaps/switch-blog-to-nextjs/zeabur-astro.jpg?v=1777738726110" alt="Zeabur Astro" width="1200" height="600" data-thumbhash="+/cFDICFiZeJeHiHd5+Q50i4Xw==">
<p>Zeabur 因为有中国区，考虑网站面向的人群，我实在是无法割舍。之前因为使用 Astro Container API，对应的包构建为 Lambda 时会有依赖问题，所以一直使用 Docker 方式在 Zeabur 部署。经过和 Zeabur 的小伙伴合作，同时完成了 Zeabur 对 <code>astro/env</code> 的支持后，现在网站已经不再使用 Docker 方式，转而使用 Zeabur 提供的 Serverless 的方式运行。</p>
<h3 id="其他微调">其他微调<a href="#其他微调"><span class="icon icon-link"></span></a></h3>
<ul>
<li>文章页展示标签：标签更像是一个动态灵活的文章分类，之前设计新博客的时候有所遗漏，现在已经补上。</li>
<li>搜索结果支持分页：之前对 fuse.js 的理解不够深刻，经过仔细摸索，发现可以基于其结果做二次分页，于是不再限制搜索条数进行一页展示。</li>
<li>自定义鼠标样式：添加了箭头、手指的鼠标样式，并支持视网膜屏幕。</li>
<li>文章支持多别名：对于一篇文章，除了固定地址外，目前还支持了短链接别名。比如 <a href="https://yufan.me/papapa" rel="nofollow" target="_blank">https://yufan.me/papapa</a></li>
</ul>
<hr>
<p>写此文章前，本有一肚子关于清明三天折腾的坎坷想要倾诉。真正写下来的时候，却又没多少。一来是年龄的增长，很多东西不再像以前那么过激。二来，大部分问题基本解决。数数上次更新博客的时间，已经不知道是猴年马月。希望自己在未来的年月里，能笔耕不辍，多记录一点自己的生活。</p>
<p>至于博客载体，那都是浮云啦。</p>
<img src="https://cat.yufan.me/images/2024/04/2024040719244778.jpg?v=1777738726110" alt="mocha@新刊委託中 - 親愛なるあなたへ" width="960" height="540" data-thumbhash="m/YNFIJGeId/h4eJeYRzWUCGBg==">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/blog">博客</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/switch-blog-to-nextjs.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flink Forward Asia 2021 北京之旅]]></title>
            <link>https://yufan.me/posts/flink-forward-asia-2021</link>
            <guid isPermaLink="false">https://yufan.me/posts/flink-forward-asia-2021</guid>
            <pubDate>Sat, 25 Dec 2021 07:13:41 GMT</pubDate>
            <description><![CDATA[2021 年对我来说，是个极其不寻常的一年，12 月更是最为重要的一个月。这个月里，我的女儿桃桃出生，同时有幸前往北京参与 Flink Forward Asia 2021 线上大会的视频录制。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4321.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4322.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4325.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4363.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4364.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4365.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4366.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4367.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4368.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4369.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4370.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4372.jpg"><p>2021 年对我来说，是个极其不寻常的一年，12 月更是最为重要的一个月。这个月里，我的女儿桃桃出生，同时有幸前往北京参与 Flink Forward Asia 2021 线上大会的视频录制。</p>
<p>因为疫情的原因，原来计划年底在杭州举办的 Flink Forward Asia 2021 大会只能被迫改为线上举办。当时我原来计划在大会上讲刚刚合并到 Flink 主分支的 <code>flink-connector-pulsar</code> 相关内容，可是还没交 PPT 稿件，女儿就早于预产期提前问世。这一拖再拖，最后只能选择去北京赶视频录制的尾巴。</p>
<p>此次北京之行，既是我离开北京 5 年后的第一次回去，我依旧走过了熟悉的呼家楼地铁站。也是我第一次去新的大兴国际机场。新机场离北京很远，很远。到机场后手机直接收到了河北的短信。因为新冠疫情的缘故，机场空荡荡的，没有什么人。这也给了我机会，让我更加细致地观察这项世纪工程。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4321.jpg?v=1777738726110" alt="Flink Forward 2021 录制 - 场地" width="1707" height="1280" data-thumbhash="V7gNDYZPdXd2lneFepiId0+dBgzG">
<p>录制的地方是在东五环的一个偏僻角落，坐了半个小时的地铁才到。应该是临时拿仓库搭建的录影棚。此时十二月的北京很冷，在屋内哈口气都能看到白雾。为了保证上镜的效果，我们都是穿着 Flink Forward Asia 的大衣，里面贴满了暖宝宝录制的。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4322.jpg?v=1777738726110" alt="Flink Forward 2021 录制 - 讲师读稿" width="1707" height="1280" data-thumbhash="zbcJBYhveGV1Z5hSmXh3iFqZD0nG">
<p>此时是我前面一位小伙伴在录制，他讲的课题现在想不起来的。只记得他讲了好久好久，久到我的脚都冻得麻木，又出去走了两圈，身子还没暖起来。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4325.jpg?v=1777738726110" alt="Flink Forward 2021 录制 - 提词器效果" width="1702" height="1276" data-thumbhash="HvgJDYJNhnUgaGdZiWaZZ9ggngu3">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4363.jpg?v=1777738726110" alt="大兴机场（一）" width="1707" height="1280" data-thumbhash="HAgKFYI4eWmfeXeDiJh3ZmiAcgYp">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4364.jpg?v=1777738726110" alt="大兴机场（二）" width="1280" height="1707" data-thumbhash="HvgFFQJkZq54CYa4h4Voh1pCkAYU">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4365.jpg?v=1777738726110" alt="大兴机场（三）" width="1280" height="1707" data-thumbhash="HggGDQAH6mZ7FZeWeIV3dw1D9SJU">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4366.jpg?v=1777738726110" alt="大兴机场 - 庆丰包子铺" width="1707" height="1280" data-thumbhash="2hgKBYB5eXhwh3d6aDiImwHID4++">
<p>拍摄此张照片的时候，某位应该还在位上。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4367.jpg?v=1777738726110" alt="大兴机场 - 登机口" width="1707" height="1280" data-thumbhash="1/cJDYKFNZmAlnmSiDmYfq9l+VmW">
<p>大兴机场的登机口设计和传统的中国机场登机口设计有所不同。它是同层出入的，也就是登机和下飞机都从这同一个门进出到同一层，而非以前的走过廊桥后，会到错开走到楼下那层离港。</p>
<p>拍摄此照片时，一开始我以为我看错了，后面专门去搜索，才发现是一种提高客流量大设计。可惜此时疫情的大兴机场没有人，无法看到流量高峰时期的胜景。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4368.jpg?v=1777738726110" alt="大兴机场（四）" width="1280" height="1707" data-thumbhash="XQgGFQKgd21lKXsY1I4myPZwaw+n">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4369.jpg?v=1777738726110" alt="大兴机场 - 壁画" width="1707" height="1280" data-thumbhash="XwgKFYJ6h3d4iIePdyiYh5J2AFkI">
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4370.jpg?v=1777738726110" alt="开始登机" width="1707" height="1280" data-thumbhash="2ygGJYQHN2l9eniAeWl4aFhwlwNn">
<p>此时透过廊桥的玻璃往外看，太阳已经完全落下，点点余晖也即将消散。淡黄色的照明灯已经亮起，让人分不清是最后那点阳光，还是灯光。一天不到的北京之行即将落幕，就像是做了一场梦一般迷幻。</p>
<img src="https://cat.yufan.me/images/recaps/flink-forward-asia-2021/IMG_4372.jpg?v=1777738726110" alt="空中俯瞰北京" width="1707" height="1280" data-thumbhash="UhgGDYBfd3dnh4hriWaIggl6vuDH">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/flink">Flink</category>
            <category domain="https://yufan.me/tags/beijing">北京</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/flink-forward-asia-2021.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[編碼三境界]]></title>
            <link>https://yufan.me/posts/three-stage-of-programming</link>
            <guid isPermaLink="false">https://yufan.me/posts/three-stage-of-programming</guid>
            <pubDate>Fri, 19 Jan 2018 01:20:12 GMT</pubDate>
            <description><![CDATA[很多人停留在第一階段，也就是能寫出來，能用。但是代碼邏輯不精簡，質量一般，同時雜亂無章。典型的特點是寫之前毫無想法，隨想隨寫。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019051818123745.jpg"><img src="https://cat.yufan.me/images/2019/05/2019051818123745.jpg?v=1777738726110" alt="Working Girl" width="1013" height="670" data-thumbhash="JBgODYJfWFmrZ3iGh3iHhoOPCXen">
<h1 id="编码的三重境界">编码的三重境界<a href="#编码的三重境界"><span class="icon icon-link"></span></a></h1>
<p>在程序员的成长过程中，写代码的能力通常会经历三个阶段：</p>
<h3 id="1-由少写多--开始懂得写代码">1. 由少写多 —— 开始懂得写代码<a href="#1-由少写多--开始懂得写代码"><span class="icon icon-link"></span></a></h3>
<p>这个阶段的特点是“能写出来，能用”。很多人停留在这里，写的代码逻辑不够精简，质量一般，而且往往杂乱无章。典型表现是：写之前毫无规划，想到什么就写什么。虽然功能能实现，但可读性和可维护性都很差。</p>
<h3 id="2-由多写少--学会优化逻辑">2. 由多写少 —— 学会优化逻辑<a href="#2-由多写少--学会优化逻辑"><span class="icon icon-link"></span></a></h3>
<p>进入第二阶段，你会开始有意识地精简代码逻辑、优化思路。代码看起来更简洁，重复和冗余减少了。但是，这种刻意追求简洁的做法，容易带来新的问题：</p>
<ul>
<li>异常情况考虑不周全</li>
<li>可维护性下降</li>
<li>代码虽然短，但阅读困难，逻辑复杂
很多程序员在这个阶段陷入“面条代码”的陷阱：为了追求短小精悍，反而让代码变得难以理解。</li>
</ul>
<h3 id="3-由少写多--掌握抽象与设计">3. 由少写多 —— 掌握抽象与设计<a href="#3-由少写多--掌握抽象与设计"><span class="icon icon-link"></span></a></h3>
<p>第三阶段的程序员，写代码前就有清晰的需求和思路。</p>
<ul>
<li>适度抽象，考虑未来扩展</li>
<li>逻辑条理清晰，易于维护</li>
<li>不一定是最简洁的代码，但一定是最优质的代码
好的代码，重在可读性、可维护性和扩展性，而不是极致的简洁。</li>
</ul>
<hr>
<h3 id="总结">总结<a href="#总结"><span class="icon icon-link"></span></a></h3>
<p>我发现一个有趣的现象：优秀的代码，其编程风格往往相似。写代码的关键，不在于技巧，而在于思路——你知道自己想要什么，知道要解决什么问题。</p>
<p>没有思路写出来的代码，即便再好看，也无济于事。</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/thinking">思考</category>
            <category domain="https://yufan.me/tags/encoding">编码</category>
            <category domain="https://yufan.me/tags/programming">编程</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/three-stage-of-programming.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[OneAPM 试用期工作小结（2015年）]]></title>
            <link>https://yufan.me/posts/first-three-month-in-oneapm</link>
            <guid isPermaLink="false">https://yufan.me/posts/first-three-month-in-oneapm</guid>
            <pubDate>Sun, 12 Nov 2017 10:52:46 GMT</pubDate>
            <description><![CDATA[最近整理文档，突然看到这份当年写的试用期述职PPT。当时才加入 OneAPM 什么都不知道，姜宁老师带着我步入微服务的大门，知道了 Gateway，了解了 Openresty。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019050902371572.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/01.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/02.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/03.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/04.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/05.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/06.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/07.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/08.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/09.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/10.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/11.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/12.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/13.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/14.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/15.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/16.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/17.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/18.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/19.jpg"><img src="https://cat.yufan.me/images/2019/05/2019050902371572.jpg?v=1777738726110" alt="大鲸鱼" width="1784" height="1242" data-thumbhash="JKcJDYKYaJiRY5lwh2iHiLZyIL9F">
<p>最近整理文档，突然看到这份当年写的试用期述职PPT。当时才加入 OneAPM 什么都不知道，姜宁老师带着我步入微服务的大门，知道了 Gateway，了解了 Openresty。</p>
<p>那个时候是 15 年年底，任何一个东西对我来说都是全新的领域。也借此认识了春哥，认识了今日头条的 Sumory 等人。对我来说这三个月的试用期弥足珍贵，因此我也在此将当时的种种一并分享出来，纪念当时那段年轻时光。</p>
<p><a href="https://cat.yufan.me/uploads/oneapm-first-three-month-work.pdf" rel="nofollow" target="_blank">PPT 等文件下载</a></p>
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/01.jpg?v=1777731762897" alt="" width="1024" height="768" data-thumbhash="PPgBBYCYd2h5h3d5hleYgKl6n6r3">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/02.jpg?v=1777731762927" alt="" width="1024" height="768" data-thumbhash="OfgFBYAvx9PEdVlKivWyeGeVMIgG">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/03.jpg?v=1777731762963" alt="" width="1024" height="768" data-thumbhash="PPgFBYB3d5d3h4h5h3eIgBAhBhEy">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/04.jpg?v=1777731762993" alt="" width="1024" height="768" data-thumbhash="PfgBBYCIZ1eHd4d2hneHgPh4gJ8H">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/05.jpg?v=1777731763025" alt="" width="1024" height="768" data-thumbhash="9fcJBYL3eXl4hnh+Z2eHndJfYwmH">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/06.jpg?v=1777731763055" alt="" width="1024" height="768" data-thumbhash="OvgFBYCgm4qbdnepd0eXqpBXcVUG">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/07.jpg?v=1777731763086" alt="" width="1024" height="768" data-thumbhash="PPgBBYCGiGh3h4h3dmaIcFZmYGUG">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/08.jpg?v=1777731763120" alt="" width="1024" height="768" data-thumbhash="PPgBBYChrziEeXiiSLVnhVNUYEUH">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/09.jpg?v=1777731763150" alt="" width="1024" height="768" data-thumbhash="O/gFDYDUngSJZoesM0aXdCtmAK/o">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/10.jpg?v=1777731763188" alt="" width="1024" height="768" data-thumbhash="OvgFBYKizniJd3fPNlOGYvaaBYfo">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/11.jpg?v=1777731763219" alt="" width="1024" height="768" data-thumbhash="PPgBBYCFeVh3d4iGZXeIgEZ2YGQH">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/12.jpg?v=1777731763250" alt="" width="1024" height="768" data-thumbhash="PPgBBYB4d2eId4h2h3eHgP190f4G">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/13.jpg?v=1777731763287" alt="" width="1024" height="768" data-thumbhash="PPgBBYBHvWZ3iIdnSFZ3kHRlkJT3">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/14.jpg?v=1777731763317" alt="" width="1024" height="768" data-thumbhash="PPgBBYCVmmh1iHeZdpV4gGZlYFYG">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/15.jpg?v=1777731763347" alt="" width="1024" height="768" data-thumbhash="9fcJDYCflJiyWIhMl/dzao+x+xer">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/16.jpg?v=1777731763377" alt="" width="1024" height="768" data-thumbhash="O/gBBYC1qGl9dYd3hVV3kFZ3YHUH">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/17.jpg?v=1777731763407" alt="" width="1024" height="768" data-thumbhash="O/gBBYCTmoh6h4eNdVZ3kFRlUFUG">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/18.jpg?v=1777731763441" alt="" width="1024" height="768" data-thumbhash="PPgBBYCUemp6dniWZTeXgFZ2YGUH">
<img src="https://cat.yufan.me/images/slide/first-three-month-in-oneapm/19.jpg?v=1777731763478" alt="" width="1024" height="768" data-thumbhash="O/gFBYC3eUeHiHeLZHeHcFd3cHUH">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/work-summary">工作总结</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/first-three-month-in-oneapm.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[OneAPM 工作两年总结]]></title>
            <link>https://yufan.me/posts/two-years-in-oneapm</link>
            <guid isPermaLink="false">https://yufan.me/posts/two-years-in-oneapm</guid>
            <pubDate>Tue, 17 Oct 2017 12:52:36 GMT</pubDate>
            <description><![CDATA[掐指一算，从 OneAPM 离职也快一个月了，在 OneAPM 工作的种种，仿佛还像是在昨天。细数两年的工作经历，我很庆幸在恰当的时间点和这么一群有激情有活力的人共事。那么，是时候总结一下我在 OneAPM 做的牛（cai）逼（ji）事情了。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019051818253816.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/01.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/02.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/03.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/04.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/05.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/06.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/07.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/08.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/09.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/10.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/11.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/12.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/13.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/14.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/15.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/16.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/17.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/18.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/19.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/20.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/21.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/22.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/23.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/24.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/25.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/26.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/27.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/28.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/29.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/30.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/31.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/32.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/33.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/34.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/35.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/36.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/37.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/38.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/39.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/40.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/41.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/42.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/43.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/44.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/45.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/46.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/47.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/48.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/49.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/50.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/51.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/52.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/53.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/54.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/55.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/56.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/57.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/58.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/59.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/60.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/61.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/62.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/63.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/64.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/65.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/66.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/67.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/two-year-in-oneapm/68.jpg"><img src="https://cat.yufan.me/images/2019/05/2019051818253816.jpg?v=1777738726110" alt="南小鸟" width="1500" height="1062" data-thumbhash="NggGDYKGvJg4eoefdnacnN0P6fY3">
<p>掐指一算，从 OneAPM 离职也快一个月了，在 OneAPM 工作的种种，仿佛还像是在昨天。细数两年的工作经历，我很庆幸在恰当的时间点和这么一群有激情有活力的人共事。那么，是时候总结一下我在 OneAPM 做的牛（cai）逼（ji）事情了。</p>
<p><a href="https://cat.yufan.me/uploads/two-years-in-oneapm.pdf" rel="nofollow" target="_blank">PPT 下载</a></p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/01.jpg?v=1777731764716" alt="" width="1366" height="768" data-thumbhash="/PcBBIADYmp/RnRusyoRbxrylw==">
<p>大家好，今天由我来分享一下，我在上家公司做的 Ai 和 告警 相关的一些内容。</p>
<p>首先，我先简单介绍一下，今天我要分享的两个项目：</p>
<ol>
<li>Ai 是 OneAPM 服务器端应用性能监控分析程序，它主要是能收集Java、CSharp、Python等偏后端语言的系统的一些指标数据。然后分析出调用 Trace 和完整的调用拓扑图，还有一些其他图表数据的展示。</li>
<li>告警系统原先作为一个 Ai 系统的子模块，用的是流式计算框架 Flink，后面不能满足对外交付和业务功能需求。我们就重新设计开发了纯粹的CEP计算引擎，依托于此在Ai上构建了新的告警系统，然后服务化拆分成独立的告警系统，并接入了其他类似Ai的业务线。</li>
</ol>
<p>这次分享，一是我对以前2年工作的整理和思考，二也是和大家交流学习。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/02.jpg?v=1777731764749" alt="" width="1366" height="768" data-thumbhash="/fcBBICC2ty/iTZbdm1kv3b2Sw==">
<p>对于 Ai，我不属于它的主要研发，我只是在上面剥离开发了现有的告警系统。所以我就讲讲我接触过的架构部分的演进。本身，就功能部分，其实没有东西。
我在说告警的时候会说的比较细一些。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/03.jpg?v=1777731764781" alt="" width="1366" height="768" data-thumbhash="/vcBBID3N7iLdnd6hIn4iYyfyA==">
<p>我是15年年底入职OneAPM，17年9月初离职加入咱们这个团队。这期间Ai伴随着业务的需求，也进行了三次大的技术架构演进。最明显的，就是每次演进中，Ai对应的存储在不断变化。同时，比较巧的是，每次架构变化的同时，我们的数据结构也略有不同，并且学习的国外竞品也不大一样。</p>
<p>说老实话，我们每次改变的步子都迈的略大，这中间也走了不少弯路。很多技术、框架，一开始看十分好，但是却不一定契合我们的需求。项目在变革初期就拆分出SaaS和企业级两套代码，并且各自都有比较多的开发分支，这些东西的维护，也让我们的代码管理一度崩溃。</p>
<p>但是，我这里主要想分享的，就是我们在业务和数据量不断增长的同时的架构设计变化，以及最后如何实现灵活部署，一套代码适配各种环境。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/04.jpg?v=1777731764846" alt="" width="1366" height="768" data-thumbhash="/PcBBIDXpoePd3ZKhnr4n4zvuQ==">
<p>OneAPM 在 2013 年开始涉足 APM 市场，当时在13年做了我们的第一代产品 Si ，它是那种庞大的单体应用，功能也十分单一。</p>
<p>在 2014 年初 OneAPM 基于用户需求和学习国外同类产品 NewRelic 开发了第一版 Ai 3.0。它的架构非常简单，就是一个收集端收集探针的数据写入Kafka，然后落到HBase里，还有一个数据展示端直接查询HBase的数据做展示。</p>
<p>在 2015 年初的时候，企业版开始做架构演进，首先是在存储这块，对于之前用 HBase 的聚合查询部分改用 Druid，对于 Trace 和 Transaction 数据转而使用 MySQL，同时，我们学习国外竞品 dynaTrace 完善了我们的分析模型。</p>
<p>2016 年的时候，我们发现存储是比较大的问题，无论是交付上，还是未来按照数据量扩容上。且 Druid 的部署、查询等都存在一些问题。在SaaS上线Druid版本之后，我们调研各类存储系统结合业务特点选用ClickHouse，并基于它开发了代号为金字塔的查询和存储模块。</p>
<p>2017 年的时候，我们开始梳理各个业务系统、组件，将它们全部拆分，公共组件服务化、Boot化，打通了各个系统。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/05.jpg?v=1777731764878" alt="" width="1366" height="768" data-thumbhash="/fcBBICnmsC2iGlLhj/xagGsZg==">
<p>这是2014年初期的第一次封闭开发后的架构，当时正好大数据Hadoop之类的比较火，所以初期的架构我们完全是基于它来做的。我们的前端应用分为 Data Collector 数据收集端，Data Viewer 数据展示层。探针端走 Nginx 将数据上传至DC来进行分析处理，页面访问通过DV获取各种数据。
Data Viewer 初期是直接读取 HBase 的，后面进行简化，部分热数据（最近5分钟调用统计），缓存于 Redis。</p>
<p>这里要提一下它和云迹的应用性能分析的区别，我们为了减少HTTP请求量和流量（小公司）探针端做了聚合和压缩，一分钟上传一个数据包。所以DC端变为解包，然后写入Kafka，对于最新的 Trace 数据，我们写入 Redis 用于快速查询生成拓扑图。</p>
<p>Consumer 主要是处理翻译探针的 Metric 数据，将其翻译为具体的监控指标名称，然后写入 HBase。</p>
<p>这套架构部署到 SaaS 之后，我们的市场部就开始推广，当时的日活蛮高，几十万独立 IP。瞬间，我们就遇到了第一个直接问题——HBase 存在写入瓶颈，HBase在大量数据持续写入的场景下，经常OOM，十分痛苦。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/06.jpg?v=1777731764910" alt="" width="1366" height="768" data-thumbhash="/PcBBICZnNCZmXhedWrjPwTrtA==">
<p>我们开始分析问题，首先，写入上，我们拆成了如图所示的三大部分，而不是之前的单一 HBase。</p>
<p>而就OLAP系统而言，数据读写上最大的特点就是写多读少，实时性要求不高。所以，查询中，HBase主要的性能问题是在对于历史某条具体的 Trace 调用指标的查询（也就是 Select One 查询）。我们在系统中引入了 MySQL，Metric 数据开始双写 HBase 和 MySQL。Redis 负责生成最新的调用拓扑，只有一条最新的 Trace 记录，MySQL 存储 Metric 数据，HBase 存储所有的 Trace 和 Metric 数据进行聚合查询。DV 还会将一些热查询结果缓存于 Redis 中。</p>
<p>这个时候的 Consumer 开始负责一定量的计算，会分出多个 Worker 在 Kafka 上进行一些处理，再将数据写入 Kafka，HBase 改为消费 Kafka 的数据。（这么做的目的，就是为了在线上拆分出不同的 Consumer 分机器部署，因为 SaaS 上的数据量，连 Consumer 都开始出现瓶颈。）</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/07.jpg?v=1777731764948" alt="" width="1366" height="768" data-thumbhash="/PcBBIDJ15CMaJhMhXeibADKRg==">
<p>在这个时候，我们引入了 Camel 这个中间件，用它将 Kafka 的操作，MySQL 的操作，还有和 Redis 的部分操作都转为使用 Camel 操作。在我介绍为什么使用 Camel 之前，我想先简单介绍一下它。（下一页PPT）</p>
<p>我们在引入 Camel 的时候，主要考虑几个方面：</p>
<p>第一，屏蔽Kafka这一层。当时SOA还比较流行，我们希望能找到一个类似 ESB 的设计，能将各个模块的数据打通。就比如MQ，它可能是Kafka，也可能是 RabbitMQ，或者是别的东西，但是程序开发人员不需要关心。
第二，我们希望一定程序上简化部署运维的麻烦，因为所有的 camel 调用 Route 的核心，就是 URL Scheme，部署配置变为生成 URL。而不是一个个变量属性配置。
第三，camel 自身的集成路由，可以实现比较高的可用性，它有多 Source 可以定义选举，还有 Fallback，可以保证数据尽可能不会丢失。（我们就曾经遇到 Kafka 挂了丢数据的情况，大概丢了3个小时，后面通过配置失败写文件的 camel 策略，数据很大程度上，避免了丢失。）</p>
<p>而且，上面的功能，基本都是写Camel DSL，而无需修改业务代码。核心就是一个词——解耦。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/08.jpg?v=1777731764979" alt="" width="1366" height="768" data-thumbhash="/PcBBIDZs+tPp4h+dJr9Lf6c4w==">
<p>Camel 用官方的话来说，就是基于 Enterprise Integration Patterns 的 Integration Framework。在我看来，Camel 在不同的常见中间件上实现集成，Camel 自身定义好链路调用 DSL（URL Scheme 和 Java、Scala、Spring XML 的实现），还有核心的企业级集成模式的设计思想，组成了 Camel 这个框架。</p>
<p>我们通过定义类似右侧的数据调用路由，将Kafka等各类中间件完全抽象出来，应用程序的逻辑转为，将数据存入Camel Producer，或者从 Camel Router 中注册 Endpoint 获取数据，处理转入另一个数据 Endpoint。（回到前面的架构图）</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/09.jpg?v=1777731765009" alt="" width="1366" height="768" data-thumbhash="/fcBBIBgptyOeIiNiWWojADYmA==">
<p>当然我们在开发过程中也设计了很多很有意思的小工具，Mock Agent 便是其中之一。</p>
<p>当时我们经常遇到的开发测试问题是，测试不好造数据来进行测试，无法验证某些特定指标的数据，开发无法脱离探针团队单独验证新功能和数据。于是我们决定自己写一套探针数据生成器，定义了一套DSL语言，完整地定义了应用、探针等数据格式，并能自动按照定义规则随机生成指定数据到后端。</p>
<p>测试需要做的事情，就是写出不同的模拟探针模板。第一，简化了测试。第二，将测试用例能代码化传承。避免人员流动的问题。</p>
<p>后面基于它，我们还写了超级有意思的压测工具，用其打数据测试后端。还有自动化测试等。</p>
<p>当然，这也是我们尝试开发的第一个 DSL。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/10.jpg?v=1777731765062" alt="" width="1366" height="768" data-thumbhash="/fcBBIDDp/qdeXdLg3jkn1f+Sg==">
<p>主要是我们无法避免写入热点问题，即使基于 Row Key 进行了写入优化，大数据量的写入也常常把 HBase 搞挂。</p>
<p>最关键的是，持续的 OOM 丢数据，已经给我们的运维带来的太多麻烦，对外的 SLA 也无法保证。（这个时间段你经常听到外面对OneAPM的评价就是数据不准，老是丢数据。）</p>
<p>基于 HBase 的查询时延也越来越高，甚至某种程度上，已经不大能支撑新的数据量。当时最高峰的时候，阿里云机器数量高达 20 台。所以，是时候考虑引入新的数据库了。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/11.jpg?v=1777731765094" alt="" width="1366" height="768" data-thumbhash="PAgGDIL2mHVrpmpbhYYGp1JwKg==">
<p>这个时候，来自 IBM 研究院的刘麒赟向我们推荐了Druid，并在我们后面的实践中取代了 HBase 作为主要的 Metric 存储。</p>
<p>2015年的时候 Druid 架构主要就是上述这张图，Druid 由4大节点组成， Real-time、Coordinator、Broker、Historical 节点，在设计之初就考虑任何一个节点挂了，不会影响其他节点。</p>
<p>Druid 对于数据的写入方式有两种，一种是实时的，直接写入 Real-time 节点，对应的是那种写多读少的数据，还有一种是批量的直接写入底层数据存储的方式，一般是对应读多写少的数据。这两种方式在 OneAPM 都有涉及，Ai 作为应用性能监控，对应的是海量的探针数据，主要是使用实时写入。Mi 是移动端性能监控，探针上传数据存在时延等问题，所以是在上层做了简单的处理缓冲后，批量写入 Deep Storage。</p>
<p>Real-time 节点主要接受实时产生的数据，例如 Kafka 中的数据。数据会在实时节点的内存中进行缓存处理，构建 memtable，然后定时生成 Segment 写入 Deep Storage。写入 Deep Storage 的数据会在 MySQL 生成 meta 索引。</p>
<p>Deep Storage 一般是 HDFS 或者是 NFS，我们在查询的时候，数据来源于 Deep Storage 或者是 Real-time 节点里面的数据。</p>
<p>协调节点主要是用于将 Segment 数据在 Historical 节点上分配，Historical 节点会自行动态从 Deep Storage 下载 Segment 至磁盘，并加载到内存查询提供结果。</p>
<p>Broker Nodes 只提供对外的查询，它不保存任何数据，只会对部分热点数据做缓存。它会从 Realtime 节点中查询到还在内存未写入 Deep Storage 的数据，并从 Historical 节点插入已经写入 Deep Storage 的数据，然后聚合合并返回给用户。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/12.jpg?v=1777731765130" alt="" width="1366" height="768" data-thumbhash="/QcCBID1RfSNd8yRaUhIi4BkBw==">
<p>所以，我们可以看到数据写入和查询遵循上面的数据流图，这里我们没有把协调节点画出。</p>
<p>数据在 Druid 上的物理存储单位为 Segment，他是基于 LSM-Tree 模型存储的磁盘最小文件单位，按照时间范围划分，连续存储在磁盘上。
在逻辑上，数据按照 DataSource 为基本存储单元，分为三类数据：</p>
<ol>
<li>Timestamp：时间戳，每条数据都必须有时间。</li>
<li>Dimension：维度数据，也就是这条数据的一些元信息。</li>
<li>Metric：指标数据，这类数据将在 Druid 上进行聚合和计算，并会按照一定的维度聚合存储到实际文件中。</li>
</ol>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/13.jpg?v=1777731765162" alt="" width="1366" height="768" data-thumbhash="/fcBBID0ltd6erhcgpn0ilmviA==">
<p>除了上述说的查询方式 OLAP 的数据其实有几大特性很关键：</p>
<ol>
<li>不可变，数据一旦产生，基本上就不会变化。换言之，我们不需要去做UPDATE操作。</li>
<li>数据不需要单独的删除操作。</li>
<li>数据基于时间，每条数据都有对应的时间戳，且每天的数据量极高</li>
</ol>
<p>所以，对于一个 OLAP 系统的数据库，它需要解决的问题也就两个维度：写入 和 查询。</p>
<p>对于 Druid 而言，它支持的查询有且不仅有上面的四种方式。但是，我们进行梳理后发现，OneAPM的所有业务查询场景，都可以基于上述四种查询方式组合出来。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/14.jpg?v=1777731765193" alt="" width="1366" height="768" data-thumbhash="/vcBBIDDpJmPipg3gobDj0L9GQ==">
<p>于是在基于 Druid 开发的时候我们遇到的第一个问题就是 Druid 的查询方式是 HTTP，返回结果基本是 JSON。我们用 Druid 比较早，那个时候的 Druid 还不像现在这样子，支持 SQL 插件。</p>
<p>我们需要做的第一个事情，就是如何简化这块集成开发的难度。我们第一时间想到的就是，在这上面开发一套 SQL 查询语法。我们使用 Antlr4 造了一套 Druid SQL，基于它可以解析为直接查询 Druid 的 JSON。</p>
<p>并基于这套 DSL 模型，我们开发了对应的 jdbc 驱动，轻松和 MyBatis 集成在一起。最近这两周，我也尝试在 ES 上开发了类似的工具，SQL 模型与解析基本写完了：<a href="https://github.com/syhily/elasticsearch-jdbc" rel="nofollow" target="_blank">https://github.com/syhily/elasticsearch-jdbc</a></p>
<p>当然这种实现不是没有代价的，我的压测的同事和我抱怨过，这种方式相比纯 JSON 的方式，性能下降了 50%。我觉得，这里我们当时这么设计的首要考虑，是在于简化开发难度，SQL对每个程序员都是最熟悉的，其次，我们还有一层考虑就是未来更容易适配别的存储平台，比如 ES（当时其实在15年中旬的时候也列入我们的技术选型）。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/15.jpg?v=1777731765226" alt="" width="1366" height="768" data-thumbhash="/PcBBICewpiKiYgfpmqPev6p5w==">
<p>Druid 另一个比较大的问题就是，它实在是太吃硬件了。记得之前和今日头条的广告部门研发聊天，聊到的第一个问题就是 Druid 的部署需要 SSD。</p>
<p>我们在前面的架构分析当中很容易发现，Druid 本质上还是属于 Hadoop 体系里面的，它的数据存储还是需要 HDFS，只是它的数据模型基于 LSM-Tree 做了一些优化。换言之，它还是很吃磁盘 IO。每个 Historical 节点去查询的时候，都有数据从 Deep Storage 同步的过程，都需要加载到内存去检索数据。虽然数据的存储上有一定的连续性，但是内存的大小直接决定了查询的快慢，磁盘的 IO 决定了 Druid 的最终吞吐量。</p>
<p>另外一个问题就是，查询代价问题。Druid 上所有的数据都是要制定聚合粒度的，小聚合粒度的数据支持比它更大粒度的聚合数据的查询。</p>
<p>比如说，数据是按照1分钟为聚合粒度存储的化，我们可以按照比1分钟还要长的粒度去查询，比如按照5分钟一条数据的方式查询结果。但是，查询的时间聚合单位越大，在分钟的聚合表上的代价也就越高，性能损失是指数级的。</p>
<p>针对上面两个问题，我们的最终解决方案，就是数据不是写一份。而是写了多份，我们按照业务的查询间隔设置了3~4种不同的聚合表（SaaS和企业级的不同）。查询的时候按照间隔路由到不同的 Druid 数据表查询。某种程度上规避了磁盘 IO 瓶颈和查询瓶颈。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/16.jpg?v=1777731765258" alt="" width="1366" height="768" data-thumbhash="O/gBBIDbp0d4mYc/lGpDW2D7cg==">
<p>在充分调研和实践后，有了上面的新架构图。3.0 到 4.0 的变化主要在HBase存储的替换，数据流向的梳理。</p>
<p>我们将探针的数据分为三大类，针对每类的数据，都有不同的存储方式和处理方式。</p>
<p>探针上传的数据，分为三大类，Trace、Metrics、Analytic Event。Trace 就是一次完整的调用链记录，Metrics 就是系统和应用的一些指标数据。Analytic 数据使我们在探针中对于一些慢 Trace 数据的详细信息抓取。最终所有的 Metrics 数据都写入 Druid，因为我们要按照不同的查询间隔和时间点去分析展示图表。Traces 和事物类信息直接存储 MySQL，它对应的详细信息还需要从 Druid 查询。对于慢 Trace 一类的分析数据，因为比较大，切实时变化，我们存入到 Redis 内。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/17.jpg?v=1777731765288" alt="" width="1366" height="768" data-thumbhash="/fcBBICwy6l8mod4dniQixG5CA==">
<p>但是，Druid 一类的东西从来都不是一个开箱即用的产品。我们前面在进行数据多写入优化，还有一些类似 SelectOne 查询的时候，越来越发现，为了兼容 Druid 的数据结构，我们的研发需要定制很多非业务类的代码。</p>
<p>比如，最简单的一个例子，Druid 中查到一个 Metric 指标数据为 0，到底是这个数据没有上传不存在，还是真的为 0，这是需要商榷的。我们有些基于 Druid 进行的基线数据计算，想要在 Druid 中存储，就会遇到 Druid 无法更新的弊端。换句话说，Druid 解决了我们数据写入这个直接问题，查询上适用业务，但是有些难用。</p>
<p>针对上述这些问题，我们在16年初开始调研开发了现有的金字塔存储模块。它主要由金字塔聚合模块 Metric Store 和金字塔读取模块 Analytic Store 两部分组成。</p>
<p>因为架构有一定的传承性。所以它和 Druid 类似，我们只支持 Kafka 的方式写入 Metric 数据，HTTP JSON 的方式暴露查询接口。基于它我们改造 Druid SQL，适配了现有的存储。它的诞生，第一点，解决了我们之前对于数据双写甚至多写的查询问题。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/18.jpg?v=1777731765319" alt="" width="1366" height="768" data-thumbhash="/PcBBIDKx/ZpiJdLiXjfWX44BQ==">
<p>我们在要求业务接入金子塔的时候，需要它提供上述的数据格式定义。然后我们会按照前面定义的聚合粒度表，自动在 Backend 数据库创建不同的粒度表。</p>
<p>金字塔存储引擎的诞生，其实主要是为了 ClickHouse 服务的，接下来，请允许我先介绍一下 ClickHouse。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/19.jpg?v=1777731765349" alt="" width="1366" height="768" data-thumbhash="/fcBBIDT9qp/mYZLlGfVf1v+1w==">
<p>从某种角度而言，Druid 的架构，查询特性，性能等各项指标都十分满足我们的需求。无论是 SaaS，还是在 PICC 的部署实施结果都十分让人满意。</p>
<p>但是，我们还是遇到了很多问题。</p>
<ol>
<li>就是 Druid 的丢数据问题，因为它的数据对于时间十分敏感，超过一个指定阈值的旧数据，Druid 会直接丢弃，因为它无法更新已经持久化写入磁盘的数据。</li>
<li>和第一点类似，就是 Druid 无法删除和更新数据，遇到脏数据就会很麻烦。</li>
<li>Druid 的部署太麻烦，每次企业级的交付，实施人员基本无法在现场独立完成部署。（可以结合我们前面看到的架构图，它要MySQL去存meta，用 zk 去做协调，还有多个部署单元，不是一个简单到能傻瓜安装的程序。这也是 OneAPM 架构中逐渐淘汰一些组件的主要原因，包括我们后面谈到的告警系统。）</li>
<li>Druid 对于 null 的处理，查询出来的 6个时间点的数据都是0，是没数据，还是0，我们判断不了。</li>
</ol>
<p>所以，我们需要在企业级的交付架构中，采取更简单更实用的存储架构，能在机器不变或者更小的情况下，实现部署，这个时候 ClickHouse 便进入我们的技术选型中。</p>
<p><a href="/posts/evolution-of-data-structures-in-yandexmetrica">ClickHouse 架构解析</a></p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/20.jpg?v=1777731765381" alt="" width="1366" height="768" data-thumbhash="eggGFIKyqqdXmIifZbpal5BVCQ==">
<p>在介绍 ClickHouse 之前，我觉得有必要分享一下常见的两种数据存储结构。</p>
<p>第一种是 B+ Tree或者是基于它的扩展结构，它常见于关系型数据的索引数据结构。我们以 MySQL 的 MyISAM 引擎为例，数据在其上存储的时候分为两部分，按照插入顺序写入的数据文件和 B+ Tree 的索引。叶子节点存储数据文件的位移。当我们读取一个索引中的范围数据时，首先从索引中查出一组满足查询条件的数据文件位移，然后按照查出来的位移依次去从数据文件中查找出实际的数据。</p>
<p>所以，我们很容易发现，我们想要检索的数据，往往在数据库上不是连续的，上图显示常见的数据库磁盘中的文件分布情况。当然我们可以换用 InnoDB，它会基于主机定义的索引，写入顺序更加连续。但是，这势必会导入写入性能十分难看。事实上，如果拿关系型数据库存储我们这种类似日志、探针指标类海量数据，势必会遇到的问题就是写入快，查询慢；查询快，写入慢的死循环。而且，扩容等操作基本不可能，分库分表等操作还会增加代码复杂度。</p>
<p>所以，在非关系型数据库里面，常见的存储结构是 LSM-Tree（Log-Structured Merge-Tree）。首先，对于磁盘而言，顺序写入的性能是最理想的。所以常见的 NoSQL 都是将磁盘看做一个大的日志，每次直接在后端批量增加新的数据以达到连续写入的目的。但就和 MyISAM 一样，会遇到查询时的问题。所以 LSM-Tree 就应运而生。</p>
<p>它在内存中和磁盘中分别使用两种不同的树结构存储数据，并同时对外提供查询能力。如 Druid 为例，在内存中的数据，会按照时间范围去聚合排序。然后定时写入磁盘，所以在磁盘中的文件写入的时候已经是排好序的。这也是为何 Druid 的查询一定要提供时间范围，只有这样，才能选取出需要的数据块去查询。</p>
<p>当然，聪明的你一定会问，如果内存中的数据，没有写入磁盘，数据库崩溃了怎么办。其实所有的数据，会先以日志的形式写入文件，所以基本不会丢数据。</p>
<p>这种结构，从某种角度，存储十分快，查询上通过各种方式的优化，也是可观的。我记得在研究 Cassandra 代码的时候印象最深的就是它会按照数据结构计算位移大小，写入的时候，不足都要对齐数据，使得检索上有近似 O(1) 的效果。</p>
<p>昨天汤总说道 Schema On Read，觉得很好，我当时回复说，要在 HDFS 上动手脚。其实本质上就可以基于 LSM-Tree 以类似 Druid 的方式做。但是还是得有时间这个指标，查询得有时间的范畴，基于这几个特点才有可能实现无 Schema 写入。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/21.jpg?v=1777731765414" alt="" width="1366" height="768" data-thumbhash="ewgGJIKD1Wp3iIi/iJqdicCZCA==">
<p>Druid 的特点是数据需要预聚合，然后按照聚合粒度去查询。而 ClickHouse 属于一种列式存储数据库，在查询 SQL 上，他和传统的关系型数据库十分类似（SQL引擎直接是基于MySQL的静态库编译的）它对数据的存储索引进行优化，按照 MergeEngine 的定义去写入，所以你会发现它的查询，就和上面的图一样，是连续的数据。</p>
<p>因为 ClickHouse 的文档十分少，大部分是俄文，当时我在开发的时候，十分好奇去看过源码。他们的数据结构本质上还是树，类似 LSM tree。印象深刻的是磁盘操作部分的源码，是大段大段的汇编语句，甚至考虑到4K对齐等操作。在查询的时候也有类似经验性质的位移指数，他们的注释就是基于这种位移，最容易命中数据。</p>
<p>对于 ClickHouse，OneAPM 乃至国内，最多只实现用起来，但是真正意义上的开发扩展，暂时没有。因为 ClickHouse 无法实现我们的聚合需求，金字塔也为此扩展了聚合功能。和 Druid 一样，在 ClickHouse 上创建多种粒度聚合库，然后存储。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/22.jpg?v=1777731765454" alt="" width="1366" height="768" data-thumbhash="O/gFDID4d4h4mHl7hIcThwCs1g==">
<p>这个阶段的架构，就已经实现了我们最初的目标，将所有的中间件解耦，我们没有直接使用 Kafka 原生的 High Level API，而是基于 Low Level API开发了 Doko MQ。目的是为了实现不同版本 Kafka 的兼容，因为我们现在还有用户在使用 0.8 的 Kafka 版本。Doko MQ 只是一层外部的封装，Backend 不一定是 Kafka，考虑到有对外去做 POC 需求，我们还原生支持 Redis 做MQ，这些都在 Doko 上实现。</p>
<p>存储部分，除了特定的数据还需要专门去操作 MySQL，大部分直接操作我们开发的金字塔存储，它的底层可以适配 Druid 和 ClickHouse，来应对 SaaS 和企业级不同数据量部署的需要。对外去做 POC 的时候，还支持 MySQL InnoDB 的方式，但是聚合一类的查询，需要耗费大量的资源。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/23.jpg?v=1777731765486" alt="" width="1366" height="768" data-thumbhash="eucJHIT5dWd3d4dug3b5eJ6P5w==">
<p>部署与交付是周一按照汤总的要求临时加的，可能 PPT 准备的不是很充分，还请大家多多包涵。</p>
<p>Java 应用部署于应用容器中，其实是受到 J2EE 的影响，也算是 Java Web 有别于其他 Web 快速开发语言的一大特色。一个大大的 war 压缩包，包含了全部的依赖，代码，静态资源，模板。</p>
<p>在虚拟化流行之前，应用都是部署在物理机上的，为了节约成本，多 war 包部署在一个 Servlet 容器内。</p>
<p>但是为了部署方便，如使用的框架有漏洞、项目 jar包的升级，我们会以解压 war 包的方式去部署。或者是打一个不包含依赖的空 war 包，指定容器的加载某个目录，这样所有的war项目公用一套公共依赖，减少内存。当然缺点很明显，容易造成容器污染。</p>
<p>避免容器污染，多 war 部署变为多虚拟机单 war、单容器。</p>
<p>DevOps 流行，应用和容器不再分离，embedded servlet containers开始流行 Spring Boot 在这个阶段应运而生。于是项目部署变为 fat jar + 虚拟机</p>
<p>Docker的流行，开始推行不可变基础设施思想，实例（包括服务器、容器等各种软硬件）一旦创建之后便成为一种只读状态，不可对其进行任何更改。如果需要修改或升级某些实例，唯一的方式就是创建一批新的实例以替换。</p>
<p>基于此，我们将配置文件外置剥离，由专门的配置中心下发配置文件。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/24.jpg?v=1777731765525" alt="" width="1366" height="768" data-thumbhash="/fcBBID4xYSHeXhcYnmwTEWUCA==">
<p>最初的时候，Docker 只属于我们的预研项目，当时 Docker 由刘斌（他也是很多中文 Docker 书的译者）引入，公司所有的应用都实现了容器化。这一阶段，我们所有的应用都单独维护了一套独立的 Docker 配置文件，通过 Maven 打包的方式指定 Profile 的方式，然后部署到专门的测试环境。换句话说，Docker 只是作为我们当时的一种测试手段，本身可有可无。</p>
<p>2015年上半年，红帽的姜宁老师加入 OneAPM，他带来了 Camel 和 AcmeAir。AcmeAir 本来是 IBM 对外吹牛逼卖他的产品的演示项目，Netflix 公司合作之后觉得不好，自己开发了一套微服务架构，并把 AcmeAir 重写改造成它组件的演示项目，后面 Netflix 全家桶编程了现在很多北京企业在尝试的 Spring Cloud。而 AcmeAir 在 PPT 中的 Docker 部署拓扑也成了我们主要的学习方式。</p>
<p>那个时候还没有 docker-compose、docker-swarm，我们将单独维护的配置文件，写死的配置地址，全部变为动态的 Hosts，本质上还是脚本的方式，但是已经部分实现服务编排的东西。</p>
<p>然后我们开始调研最后选型了 Mesos 作为我们主要的程序部署平台，使用 Mesos 管理部署 Docker 应用。在上层基于 Marthon 的管理 API 增加了配置中心，原有脚本修改或者单独打包的配置文件变为配置中心下发的方式。最后，Mesos 平台只上线了 SaaS 并部署 Pinpoint 作为演示项目，并未投产。</p>
<p>后面，在告警系统的立项开发过程中，因为要和各个系统的集成测试需要，我们慢慢改写出 docker-compose 的方式，废弃掉额外的 SkyDNS。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/25.jpg?v=1777731765557" alt="" width="1366" height="768" data-thumbhash="9ggGHILY2KeGeIhvdXhoqwC8Sg==">
<p>Mesos 计划的夭折，主要原因是我们当时应用还没有准备好，我们的应用主要还都是单体应用各个系统间没有打通。于是在 16年我们解决主要的存储问题之后，就开始着力考虑应用集成的问题。</p>
<p>应用服务化是我们的内部尝试，是在一次次测试部署和对外企业交付中的血泪总结。当时我们考虑过 Spring Integration，但是它和 camel 基本如出一辙，也调研过 Nexflix 全家桶，最后我们只选用了里面的 zuul 做服务网关。</p>
<p>在应用层面，我们按照上图所示，将所有的应用进行服务化拆分，分成不同的组件开发维护，并开发了注册中心等组件。RPC 这边，我们没有使用 HTTP，而是和很多公司一样包装了 Thrift。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/26.jpg?v=1777731765592" alt="" width="1366" height="768" data-thumbhash="PPgFDID1iWp2ioiMYow4iUBfdw==">
<p>我们基于前面的服务拆分，每个应用在开发的时候，都是上述5大模块。中间核心的中间件组件，业务系统均无需操心。在交付的时候，也属于类似公共资源，按照用户的数据量业务特点弹性选择。</p>
<p>最小化部署主要是为了给单独购买我们的某一产品的用户部署所采用的。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/27.jpg?v=1777731765629" alt="" width="1366" height="768" data-thumbhash="/PcBBIDAz5aJiWZtRVfwdQZtdw==">
<p>但是我们已经受够了一个项目维护多套代码的苦楚，我们希望一套代码能兼容 SaaS、企业级，减少开发中的分支管理。于是我们拆分后的另一大好处就体现了，它很容易结合投产未使用的 Mesos 在 SaaS 上实现部署。</p>
<p>为了打通各个产品，我们在原有的前后端分离的基础上，还将展示层也做了合并，最后实现一体化访问。后端因为实现了服务化，所有的应用都是动态 Mesos 扩容。CEP 等核心计算组件也能真正意义上和各个产品打通，而不是各做各的。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/28.jpg?v=1777731765661" alt="" width="1366" height="768" data-thumbhash="/vcBBIDAqqh9mpl2U4qQiwK5GA==">
<p>到了这里，我的第一阶段就算是讲完了，大家有问题么？</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/29.jpg?v=1777731765691" alt="" width="1366" height="768" data-thumbhash="/vcBBID3N7iKdnd7g4j4iYyvyA==">
<p>告警系统的开发，我们和 Ai 一样，经历了几个阶段，版本迭代的时间点也基本一致。整个开发过程中，我们最核心的问题其实不在于告警功能本身，而是其衍生的产品设计和开发设计。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/30.jpg?v=1777731765721" alt="" width="1366" height="768" data-thumbhash="uecFDILAi7hUiYeLdpywQAoLpA==">
<p>和 Ai 一样，初期的告警实现特别简单。当时来自 IBM 研究院的吴海珊加入 OneAPM 团队，带来了 Cassandra 存储，我们当时用的比较早，是 2014 年 2.0 版本的 Cassandra，我们在充分压测之后，对它的数据存储和读写性能十分满意，基于它开发了初版告警（草案）。</p>
<p>初版告警的实现原理极其简单，我们从 Kafka 接收要计算的告警指标数据，每接收到一条指标数据，都会按照配置的规则从 Cassandra 中查询对应时间窗口的历史指标数据，然后进行计算，产生警告严重或者是严重事件。然后将执行的告警指标写入 Cassandra，将告警事件写入 Kafka。（看下一页）</p>
<p>所以你会发现初版的告警，从设计上就存在严重的 Cassandra 读写压力和高可用问题。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/31.jpg?v=1777731765753" alt="" width="1366" height="768" data-thumbhash="+fcFBILFiXl3iIiPdIpQVjlb9g==">
<p>你会发现，每从业务线推送一条指标数据，我们至少要读写两次 Cassandra。和同时期的 Ai 架构相比，Ai 对 HBase 只有写入瓶颈，但读取，因为量不高，反而没有瓶颈。（回上页）</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/32.jpg?v=1777731765784" alt="" width="1366" height="768" data-thumbhash="/PcBBID3pnh2iYlbk2zGgGQNSA==">
<p>这里是我们和 Ai HBase的对比总结。我们初版的设计和 Ai 一样都需要全量地存储指标数据，而且 Cassandra 的存储分片本身是基于 Partition Key 的方式，数据必须基于 Partition Key 去查询，我们对于计算指标，按照 业务系统、应用 ID、时间 作为 Partition Key 去存储。很意外的是几乎和 HBase 同时出现了读写瓶颈。而且比较尴尬的地方也和 Ai 类似，因为 Partition Key 的定义，完全无法解决写入热点问题。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/33.jpg?v=1777731765828" alt="" width="1366" height="768" data-thumbhash="dfgJFILwe6uJiHecdnZchwDalw==">
<p>所以我们首先想到的是，对于当前的告警架构进行优化，我们有了上述的新架构设计。但是在评审的时候，我们发现，我们做的正是一个典型的分布式流式处理框架都会做的事情，这些与业务逻辑关系不大的完全可以借助现有技术实现。</p>
<p>由于这个时期（15年）公司整体投产大数据，我们自然把眼光也投入了当时流行的大数据计算平台。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/34.jpg?v=1777731765859" alt="" width="1366" height="768" data-thumbhash="uecFFILwnLllmYh6dovCQDkMlA==">
<p>首先，对于初版的架构，我们需要保留的是原有的计算数据结构和 Kafka 的写入方式，对于核心的告警计算应用需要去改造。</p>
<p>我们需要实现的是基于<strong>单条数据</strong>的实时流式计算。需要能分布式水平扩展。能按照规则分组路由，避免热点问题。需要有比较好的编程接口。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/35.jpg?v=1777731765893" alt="" width="1366" height="768" data-thumbhash="/PcBBICQqaqHeZh4h2kTfFCHCA==">
<p>首先我们考察的便是 Spark，Spark 最大的问题是需要我们人为指定计算的时间窗口，计算的数据也是批量的那种而非单条，这和告警的业务需求本身就不匹配。</p>
<p>因为当时我们想设计的告警计算是实时的，而非定时。Spark Streaming 在后面还因为执行模式进一步被我们淘汰。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/36.jpg?v=1777731765928" alt="" width="1366" height="768" data-thumbhash="PfgBBICgydtJqJqHVosnekDEBw==">
<p>Strom 各方面其实都蛮符合我们需求的，它也能实现所谓的单条实时计算。但是，它的计算节点不持有计算状态，某些时候的窗口数据，是需要有类似 Redis 一类的外部存储的。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/37.jpg?v=1777731765967" alt="" width="1366" height="768" data-thumbhash="/PcFBID1Z7l5iXZrhHT0dmNvRw==">
<p><strong>Flink优势：</strong></p>
<p>Spark 有的功能 Flink 基本都有，流式计算比 Spark 支持要好。</p>
<ol>
<li>Spark是基于数据片集合（RDD）进行小批量处理，所以Spark在流式处理方面，不可避免增加一些延时。</li>
<li>而 Flink 的流式计算跟 Storm 性能差不多，支持毫秒级计算，而 Spark 则只能支持秒级计算。</li>
<li>Flink 有自动优化迭代的功能，如有增量迭代。它与 Hadoop 兼容性很好，还有 pipeline 模式增加计算性能。</li>
</ol>
<p>这里，我需要重点说一下 pipeline 模式。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/38.jpg?v=1777731766004" alt="" width="1366" height="768" data-thumbhash="PPgFDID5g1h7lolZpHQHhfGmdg==">
<p>Staged execution 就如它的名字一样，数据处理分为不同的阶段，只有一批数据一个阶段完全处理完了，才会去执行下一个阶段。典型例子就是 Spark Streaming</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/39.jpg?v=1777731766036" alt="" width="1366" height="768" data-thumbhash="PPgFDID5hGh6lolao3YGhfKldg==">
<p>Pipeline 则是把执行串行在了一起，只有有计算资源空闲，就会去执行下一个的操作。在外部表象是只有一个阶段。</p>
<p>上面的不好理解，我们思考一个更形象的例子：</p>
<p>某生产线生产某钟玩具需要A,B,C三个步骤，分别需要花费10分钟，40分钟，10分钟，请问连续生产n个玩具大概需要多少分钟？</p>
<p><strong>总结：</strong></p>
<p>stage的弊端是不能提前计算，必须等数据都来了才能开始计算(operateor等数据，空耗时间)。pipeline的优势是数据等着下一个operateor有空闲就立马开始计算(数据等operateor ，不让operateor闲着，时间是有重叠的)</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/40.jpg?v=1777731766069" alt="" width="1366" height="768" data-thumbhash="+fcFBICjt4hyiXeLd4+GcGEJBw==">
<p>综合前面的调研分析，我们有了上面这张表格。对于我们而言，其实在前面的分析中 Flink 就已经被我们考虑了，尤其是它还有能与 Hadoop 体系很好地整合这种加分项。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/41.jpg?v=1777731766102" alt="" width="1366" height="768" data-thumbhash="8tcJHIT4tZqJh3eKhnkQpmqfuA==">
<p>综合前面的分析，我们最终选择 Flink 来计算告警，因为：</p>
<ol>
<li>高效的基于 Pipeline 的流式处理引擎</li>
<li>与 Hadoop、Spark 生态体系良好的兼容性</li>
<li>简单灵活的编程模型</li>
<li>良好的可扩展性，容错性，可维护性</li>
</ol>
<p>在架构逻辑上面，我们当时分成了上述五大块。</p>
<p>元数据管理主要指的是告警规则配置数据，数据接入层主要是对接业务系统的数据。</p>
<p>计算层主要是两类计算，异常检测：按照配置的静态阈值进行简单的计算对比、No Event 无事件监测，主要是监控应用的活动性。</p>
<p>缓存区主要是计算数据队列的缓存和应用告警状态的缓存。存储区第一块是从原有架构继承的 Cassandra。离线存储是考虑给别的大数据平台共享数据使用的。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/42.jpg?v=1777731766139" alt="" width="1366" height="768" data-thumbhash="MugJDIT3t5iLeIZ8hnpXd4D8GA==">
<p>这里画的是 Standalone 的部署方式，也是我们在本地开发测试的架构，在生产上，我们采用了 Flink on YARN 的部署模式。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/43.jpg?v=1777731766169" alt="" width="1366" height="768" data-thumbhash="PPgFBIDG+2OIeJdqdnoIc5EEpg==">
<p>对于 Flink 的任务调度，我们以左下角的一个简单操作为例，它是一个 source(4) -&gt; map(4) -&gt; reduce(3)，其调度在 Flink 中如图所示，会分成几个不同的 TaskManager 来操作，每个 TaskMananger 中有多个执行单元，但呈管道式。将外部网状的处理流程变为独立的线性处理任务。</p>
<p>我们基于 Flink 首先需要开发的，就是异常检测流程。告警的异常检测就相当于 Flink 的一个 Job（Streaming），借助 Flink 简单易用的编程模型，我们可以快速的构建我们的 Flow。</p>
<p>在设计的初期，我们考虑了几个方面的问题：</p>
<ol>
<li>作为通用的计算引擎，谁可以使用这个 Job。</li>
<li>如果后面某些产品提出一些变态的需求，我们是否可以快速开发一个针对特殊需求的 Job 提交到同一的平台去运行？</li>
<li>平台是否可以提供稳定的运行环境、可维护性、可扩展性、可监控性以及简单高效的编程模型，咱们可以把更多的精力放在两个方面：a.业务；b.平台研究（确保稳定性）</li>
<li>生产上统一到 Yarn 上之后，我们可以在一个集群上公用一份数据，根据不同场景使用不同的计算引擎做他适合做的事情。比如，暴露数据给 Spark 团队使用。</li>
<li>Akka 集群化研究，我们原有的 Akka 开发经验，不能浪费。对于企业交付，还是需要有一个小而美的程序架构。Akka 那个时候是 2.3 版本，提供了 Akka Cluster，重新被我们纳入研究范畴。</li>
</ol>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/44.jpg?v=1777731766215" alt="" width="1366" height="768" data-thumbhash="/PcBDIDC6/mJeJiPVXyUdU9Z9w==">
<p>我们遇到的第一个问题，就是多数据源，生产上提供计算数据源的可能不仅仅是 Ai 一个产品，还有别的产品。我们研究后发现，Fink 原生支持多数据源。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/45.jpg?v=1777731766246" alt="" width="1366" height="768" data-thumbhash="/AcGDIKnh2mDeIh6dY8EZEaANg==">
<p>说到Rule的问题，我们逃不开一个问题：Rule管理模块到底应不应该拆出来。</p>
<p>首先，元数据管理的压力不大，数据量也不会大到哪里去，他的更新也不是频繁的。
其次，让 Flink 在各个节点上启动一个 Web服务去更新规则是不现实的，也不值当。</p>
<p>所以，把Rule管理模块单独抽取出来是合适的。</p>
<p>抽取出来之后，自然就涉及告警计算的 Job 如何感知 Rule 变更的问题：</p>
<p>完全依赖外部存储，例如 Redis，Job 每次都去查存储获取 Rule（这样完全规避了 Rule 更新问题，但是外部存储能够扛得住是个问题，高并发下 Redis 还是会成为瓶颈）。
Job内存里自己缓存一份 Rule，并提供更新机制。</p>
<p>无论怎么搞都得依赖外部通知机制来更新 Rule，比如元数据管理模块更新完 Rule 就往 Kafka 发送一个特殊的 Event。算子收到特殊的 Event 就去数据库里把对应的 Rule 查询出来并更新缓存。</p>
<p>有了更新机制，我们要解决的就是如何在需要更新的时候通知所有的算子，难点是一个特殊的Event只能发送给一个算子实例，所以我们上面采用了单实例，存在两个问题。</p>
<ol>
<li>性能瓶颈</li>
<li>消息表变大了(key,event)—(key,event,rule)，更加消耗资源</li>
</ol>
<p>其实，我们忽略了一个问题，当Rule有更新的时候我们完全没必要通知所有的算子实例。</p>
<p>虽然我们不是一个 Rule 对应一个算子，但是 Flink 是提供分区机制的，我们已经用 key 做了hash。Rule 的更新不会更新 key，产生的特殊 Event 会分区到固定的算子具体实例，具有相同 key 的 Event 也必然被分区到相同的算子实例。所以我们的担心是多余的，而且借助分区机制，我们对内存的占用会更小，每个算子实例只缓存自己要用的Rule。</p>
<p>所以 Rule 的更新只有三种场景：初始化时不做预加载缓存，第一次使用Rule时查数据库并缓存，收到内置Event时更新缓存。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/46.jpg?v=1777731766277" alt="" width="1366" height="768" data-thumbhash="/fcBBIDF5Laee5oJtWmen6HICQ==">
<p>No Events 检测主要的问题是 Flink 是实时数据计算，他是来一条数据计算一次。无事件本身的的特点就是没有数据推送过来，无法触发计算。</p>
<p>这个问题其实已经非常好解决了，我们在告警计算的流程里已经更新每个Rule对应event的最后达到时间到Redis了。我们可以单起一个批处理job简单运算一次即可，逻辑非简单，我测了一下16000个Rule，5个并发度，可以在5s内计算一次。注：带注释才用了不到120行java代码，稍加改进即可在生产上使用。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/47.jpg?v=1777731766310" alt="" width="1366" height="768" data-thumbhash="+/cFDID1emeIeHeLdXf0h2kMtQ==">
<p>最终，我们在解决上述问题之后在阿里云上实现了上述的告警计算平台。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/48.jpg?v=1777731766342" alt="" width="1366" height="768" data-thumbhash="/fcBBIDQi6mKeomFRXnAmgS8KQ==">
<p>从某种角度而言 Flink 版本是第一个在 SaaS 上投产的系统，然而，它并不完美，有着上述这些问题。</p>
<p>从某种角度而言，Flink 计算告警有些大材小用，我们需要更轻量的架构。（这里中断，展示一下我们的告警系统。）</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/49.jpg?v=1777731766372" alt="" width="1366" height="768" data-thumbhash="/QcCBICzxXpHuYlrdc9leWCVCA==">
<p>在 Flink 版本开发3个月后，我们开始着手开发新的企业级告警平台。因为现有的 Flink 版本，因为很多原因无法对外交付。</p>
<p>我也是从这个时候开始参与 OneAPM 告警的研发，我们做的第一件事情，就是结合之前 DSL 开发的经验，思考如何重新定义告警规则。这是因为 Flink 上定义的告警规则，就和现有的云迹 CTMAM 的告警规则一样，比较死板，不好扩展，且较为复杂。</p>
<p>这期间也参考了 Esper 之类的开源项目，比较后我们惊喜地发现，最好的告警规则定义方式就是 SQL。</p>
<p>我们在定义好规则模板之后，便开始由解析计算引擎 -&gt; 处理队列引擎 -&gt; 分布式管理平台 -&gt; 操作接口的顺序 开发了现有的告警引擎。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/50.jpg?v=1777731766406" alt="" width="1366" height="768" data-thumbhash="OwgGBIB+8rQvuqNal+QnrJDiCw==">
<p>首先是基于规则 DSL 的解析计算引擎。之前的 Mock Agent，我们使用的是 Scala 原生提供的语法分析组合子设计的。Druid SQL 使用的是 Antlr4，先解析出基本的 AST 语法树，然后转义为 Druid JSON 查询模型，最后序列化为查询 JSON。</p>
<p>这里的告警规则 SQL，我们用的是类似 Druid SQL 的方式。语法模板定义甚至都是类似的。只是增加了四则混合运算表达式的解析和运算，还有 avg 一类的计算函数的实现。</p>
<p>最终，它的解析处理流程就和 PPT 图示的一样。规则 SQL 语句被 Antlr4 做词法语法分析，将部分非逻辑单元符号化，然后构建出一棵 SQL 语法树。我们按照 Antlr4 提供的 Visitor 模式，以深度优先检索的方式遍历，然后不断的将结果按照定义的算子单元组合。最后对外暴露出两个方法，一个返回布尔值表示是否满足规则运算定义，另一个返回计算中想要获取的指标数据。</p>
<p>我们基于解析出来的规则对象，在 Engine 层对计算的事件队列和当前事件结合起来，就产生了实际想要的计算结果。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/51.jpg?v=1777731766439" alt="" width="1366" height="768" data-thumbhash="/fcBBIDBqc+Keqk3dXuFimC4CA==">
<p>Engine 就相当于最小粒度的计算单元，但是，它缺少一些上下文管理。我们需要事件队列管理，规则和计算数据的关联，才能真正意义上调用 Engine 去计算。</p>
<p>基于这个需求，我们开发了 Runtime 模块。它在逻辑上有两大抽象，一个是 RuntimeContext，一个是 EventChannel。</p>
<p>RuntimeContext 就和它的名称一样，表示运行时上下文，每个RuntimeContext 对应一条具体的规则示例，内部会维护对应的 RuleTemplate。我们在设计初期就考虑类似多数据源的情况，一条计算规则可能对应多个探针数据，于是内部定义了 InputStream 的概念。</p>
<p>它相当于实际的一条计算指标数据流，实际存储在 EventChannel 上，EventChannel 为在内存中存储的一个指标数据队列。它有两块数据：一个是一个 Event Queue，一个是当前才来的一条要计算的指标数据。Event Queue 的设计参考了 Guava Cache 里面的队列，因为规则创建时对应的数据窗口大小是确定的，于是这个 Queue 的大小也是确定的。</p>
<p>一个 RuntimeContext 示例可能对应多个 EventChannel，一个 EventChannel 也可能对应多个 RuntimeContext，二者基于一个唯一的 key 关联起来。我们修改规则的时候，需要修改对应的 RuntimeContext。事件来了要计算的时候，是直接 sink 到 EventChannel 中。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/52.jpg?v=1777731766468" alt="" width="1366" height="768" data-thumbhash="/fcBBIDFp7V/i6kHdqqlmkC+Cg==">
<p>Runtime 相当于 Flink 里面最小的计算任务，有着自己的状态，能解析 SQL 并进行运算。</p>
<p>但是对于分布式、集群等部署环境，它还存在着较大的问题。在其之上，我们使用 Akka 开发了核心的运行模块。</p>
<p>我们使用 Akka Cluster 开发了计算集群，Akka Cluster 将 Akka 应用分为 Seed Node 和一般 Node。启动的时候，要先启动 Seed Node，才能启动子 Node。但是启动后如果 Seed Node 挂了，Akka 可以选出一个新的存活节点当做 Seed Node。</p>
<p>我们在 Akka 集群启动后，会使用 Seed Node 创建 Kafka Message Dispatcher Actor 来和 Kafka 消费数据，然后分发到各个子节点上。这么做的话，同一时刻，只有一个线程在从 Kafka 消费数据。使用单线程的考虑有很多，比如避免 Kafka repartition。其次，我们测试后发现，从 Kafka 消费这块使用单线程不存在瓶颈。</p>
<p>每个 Akka 节点都分为 EvenStreamActor、RuleActor 两类核心处理计算单元，EventStreamActor 除了管理 EventChannel 之外，还会将数据分发到别的 Akka 节点，做二次计算。RuleActor 管理 RuntimeContext，其下包含 Persist Actor 将告警事件和应用实时状态持久化到金字塔存储，Alert Actor 将告警数据写入至 Doko MQ 用于接入系统执行告警行为（如短信、邮件、WebHook 等）。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/53.jpg?v=1777731766499" alt="" width="1366" height="768" data-thumbhash="/fcBBID2ZDuLeZlkdXloipCWCA==">
<p>Jetty模块本身用于暴露接口对外提供规则、事件、数据源管理。和 Flink 版本一样，我们遇到了一个问题就是如何在所有的 Akka 集群上更新告警规则。</p>
<p>后面我们的实现策略和 Flink 的版本一样，规则在 Cassandra 上更新完毕后，会以特定的更新消息写入 Kafka 中。这个时期，所有的告警规则配置，使用用户，告警数据源的配置，都保存在 Cassandra 中。因为 Partition Key 的创建不大合理，也给我们在做检索，分页等操作时，尤其是告警事件的筛选，带来了极大的麻烦。这也直接导致我们在 3.0 版本里面将所有的配置数据存于 MySQL，告警事件改为使用金字塔存储。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/54.jpg?v=1777731766533" alt="" width="1366" height="768" data-thumbhash="/fcBBIBldWZ0iXh2eZ/5mKCvGQ==">
<p>基于计算引擎，我们抽象出三大逻辑模块，告警计算和管理模块、告警策略管理模块、推送行为管理模块。</p>
<ol>
<li>计算引擎，也就是前面说的 Runtime 模块那层、它只关心什么规则，基于数据算出一个 true/false 的布尔值表示是否告警，同时返回计算的指标集。</li>
<li>事件生成引擎，它基于前面的计算结果，还有指标的元数据等组合生成实际的告警事件。2.0 版本只有三种：普通、警告、严重。</li>
<li>推送行为模块，其实就是配置支持哪些通知用户的方式，在 2.0 里面只有发邮件、执行 Shell，3.0 之后支持 Web Hook 和短信。</li>
<li>策略模块，就是关联某个规则应该用那些现存的通知配置。</li>
</ol>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/55.jpg?v=1777731766562" alt="" width="1366" height="768" data-thumbhash="OQgGDIL12peLhYl6eH1HUnAUBA==">
<p>2.0 版本的告警系统主要是 CEP 计算引擎模块，所以在部署上，他是集成在各个业务系统上的。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/56.jpg?v=1777731766594" alt="" width="1366" height="768" data-thumbhash="/fcBBID1NmZ6iolLhnn3iJD9GQ==">
<p>2.0 的时候，告警系统只产生三类事件，普通事件、警告事件、严重事件。我们调研之后发现，其实用户在意的不是这类事件，而是这三类事件相互转换之后产生的事件。</p>
<p>于是我们重新定义了告警事件。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/57.jpg?v=1777731766626" alt="" width="1366" height="768" data-thumbhash="/fcBBIDzdWlpiohkY3r0fGHPKA==">
<p>所以我们将告警引擎产生的事件分为两大类：HealthStatusEvent、HealthRuleVolatation 事件。</p>
<p>前者就是图上的三个圈，也就是前面的正常、警告、严重。（做鼠标指点状）应用状态从“普通”到“严重”会产生“开启严重”事件。应用状态从“警告”到“普通”会产生“关闭警告”事件，应用状态持续在“警告”或者“严重”会产生持续类事件。我们对于告警的触发配置转为这种状态转换的事件。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/58.jpg?v=1777731766658" alt="" width="1366" height="768" data-thumbhash="/fcBBICAd4lpd4h4dYeZm7DKCg==">
<p>有了前面的设计之后，我们遇到了第一个问题，如何在现行的 Akka 应用上设计一个告警事件状态机。我们想了很多方式，后面我们发现，自己完全想岔了。</p>
<p>之前开发的 Engine 模块结合 Runtime 模块完全可以解决这个问题，我们只需要按照之前定义的 8 个事件转换状态定义 8条 SQL，配置三个子 RuntimeContext 即可解决这个问题。比如开启警告事件，它的 SQL 定义如上。也就是之前一个告警事件如果为空或者为NORMAL事件，当前这条事件为警告事件，则生成开启告警事件。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/59.jpg?v=1777731766692" alt="" width="1366" height="768" data-thumbhash="/PcBDILxu5yJeImNVGiAiQuYyA==">
<p>我们对于不同时间段应用的期望运行情况可能是不一样的，比如一天当中的几个小时，一星期中的几天或者一年当中的几个月。举个例子来说，淘宝应用在周末两天可能会存在较多的交易从而产生高于平时的吞吐量。一个工资支付应用可能相较于一个月中的其他事件，会在月初和月末产生较大的流量。一个客户管理的应用在周一的营业时间相较于周末来说会有较大的工作负荷压力。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/60.jpg?v=1777731766723" alt="" width="1366" height="768" data-thumbhash="/fcBBID2dld8mIhoc2ZUeFCWCA==">
<p>我们在 2.0 的版本开始受制于 Cassandra。</p>
<p>一方面，我们建表的时候，为了某些性能在 Partition Key 内增加了时间戳导致查询的时候必须要提供时间区间。另一方面，沿用的是2年前的 Cassandra 版本，无法像 3.0 之后的版本一样有更丰富的查询方式，比如基于某一列的查询。</p>
<p>其次，在 2.0 之前的版本，每条指标的计算结果，就算是 Normal 都会存入 Cassandra，这是因为 Flink 版本计算的遗留问题。而我们在设计了告警事件的状态变化告警之后，存储 Normal 变为意义不大。</p>
<p>最后，除了告警事件，其他的数据：如规则、策略、行为等配置数据，撑死了也就几十万条，完全没有必要用 Cassandra 来存储。它的使用，反而会增加企业级的部署麻烦。</p>
<p>所以我们进行了变更，用 MySQL 去存储除告警事件之外的数据。告警事件因为有了金字塔模块，所以我们直接写入 Kafka 即可。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/61.jpg?v=1777731766761" alt="" width="1366" height="768" data-thumbhash="/fcBBICAp7p4iqpnh2uFimCoCA==">
<p>为了应对 2.0 版本的接入麻烦，因为构造 SQL、告警通知行为等在 2.0 版本都是外包给业务线自己做的，我们只是打造了一个小而美的 CEP 引擎。所以只有主要的产品 Ai 接入了我们系统。为此，我们把 Ai 中开发的和告警相对于的代码剥离，专门打造了 CEP 上层的告警系统，并要求业务方提供了应用、指标等 API。自行消费处理 Kafka 中的告警事件，触发行为。</p>
<p>其次，做的一个很大改动就是适配了各个业务线的探针数据，直接接受全量数据。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/62.jpg?v=1777731766791" alt="" width="1366" height="768" data-thumbhash="/fcBBIDzSMiNiYZoc4eGi2C4CA==">
<p>4.0 阶段的告警其实并没有开发，当时主要协作的另一位同事在6月离职，我在8月底完成 3.0 的工作后也离职，但是设计在年初就完成了。</p>
<p>我们在开发金子塔存储的时候，很大的一个问题就是如何流式消费 Kafka 的数据，当时正好 Kafka 提供了 Stream 编程。我们使用了 Akka Stream 去开发了对应的聚合应用 Analytic Store。</p>
<p>同样，我们希望这个单独开发的 CEP 应用，也能变成 Reactive 化。对应的我们将上下行的 Kafka 分别抽象为 Source 和 Sink 层，它们可以使用 Restful API 动态创建，而非现在写死在数据库内。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/63.jpg?v=1777731766821" alt="" width="1366" height="768" data-thumbhash="/fcBBIDYuJZYiIdfpoSZR6B6BQ==">
<p>基于这一思想，我们大概有上述的技术架构（图可能不是很清晰）。</p>
<p><strong>设计目标：</strong></p>
<p>增加CEP处理数据的伸缩性（scalability）,水平伸缩以及垂直伸缩
提高CEP引擎的弹性（Resilience），也就是CEP处理引擎的容错能力</p>
<p><strong>设计思路：</strong></p>
<p>在数据源对数据进行分流（分治）；在Akka集群里，创建Kafka Conumser Group， Conumser个数与Topic的分区数一样，分布到Akka的不同节点上。这样分布到Akka某个节点到event数据就会大大减少。</p>
<p>在数据源区分Command与Event；把Rule相关到Command与采集到metric event打到不同的topic，这样当Event数据很大时，也不会影响Command的消费，减少Rule管理的延迟。</p>
<p>对Rule Command在Akka中采用singleton RuleDispatcher单独消费，在集群中进行分发，并且把注册ruleId分发到集群中每个EventDispatcher里。因为Rule Command流量相对于Event流量太少，也不会出现系统瓶颈。</p>
<p>因为RuleDispatcher在Akka集群是全局唯一的，容易出现单点故障。因为RuleDispathcer会保存注册后的RuleIds，需要对RuleId进行备份，这个可以采用PersitentActor来</p>
<p><strong>实现</strong></p>
<p>对于RuleDispatcher down掉重启的这段时间内，因为RuleDispatcher分发过RuleId到各个节点的EventDispatcher，因此各个节点事件分发暂时不会受到影响。
在Akka集群里每一个Kafka conumser，对于一个EventDipatcher，负责把事件分发对感兴趣对RuleActor（根据每个RuleId对应感兴趣对告警对象）。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/64.jpg?v=1777731766855" alt="" width="1366" height="768" data-thumbhash="O/gFDIDNoZeXeGlPlW0HiIGnCA==">
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/65.jpg?v=1777731766885" alt="" width="1366" height="768" data-thumbhash="/fcBBICE2+KvR6YmZzSpeYCpCA==">
<p>常见的聚类算法有三类：基于空间划分的、基于层次聚类的和基于密度聚类的方法。聚类算法一般要求数据具有多个维度，从而能够满足对海量样本进行距离（相似性）度量，从而将数据划分为多个类别。其中维度特征一般分为CPU利用率、磁盘利用率、I/O使用情况、内存利用率、网络吞吐量等。</p>
<ol>
<li>相似性度量方法</li>
</ol>
<p>相似性度量一般采用LP范式，如L0、L1、L2等，其一般作为两个样本间的距离度量；Pearson相关系数度量两个变量的相似性（因为其从标准分布及方差中计算得到，具有线性变换不变性）；DTW（动态时间规整算法）用于计算两个时序序列的最优匹配。
其中基于LP范式的时间复杂度最低O(D)</p>
<ol start="2">
<li>数据压缩（降维）方法</li>
</ol>
<p>在数据维度较大的情况下，通过数据压缩方法对时序序列进行降维是聚类算法的必备步骤。其中较为常用的数据降维方法有Discrete Fourier Transform, Singular Value Decomposition, Adaptive Piecewise Constant Approximation, Piecewise Aggregate Approximation, Piecewise Linear Approximation and the Discrete Wavelet Transform。下采样方法也是一类在时序序列中较为常用的技术。
降维方法的时间复杂度一般在O(nlogn)到O(n^3)不等。</p>
<ol start="3">
<li>聚类方法</li>
</ol>
<p>基于空间划分的、基于层次聚类的和基于密度聚类的方法。如 K-means，DBSCAN 等。K-Means 方法是通过对整个数据集计算聚类中心并多次迭代（时间复杂度降为O(n<em>K</em>Iterations<em>attributes)），而Incremental K-Means方法是每加入一个数据项时，更新类别中心，时间复杂度为O(K</em>n)，所以其对初始化中心不敏感，且能很快收敛。
时间复杂度一般在 O(nlogn) 到 O(n^2)</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/66.jpg?v=1777731766918" alt="" width="1366" height="768" data-thumbhash="/vcBBICQi6mMiImERXiAjALYGA==">
<p>之前看 Openresty 的作者章亦春在 QCon 上的分享，他谈到的最有意思的一个观点就是面向 DSL 编程方式。将复杂的业务语义以通用 DSL 方式表达，而非传统的重复编码。诚然，DSL 不是万金油，但是 OneAPM 的告警和 Ai 数据分析，很大程度上受益于各类 DSL 工具的开发。通过抽象出类 SQL 的语法，才有了非常可靠的扩展性。</p>
<p>Akka 和 Scala 函数式编程的使用，很大程度上简化了开发的代码量。我在16年年初的时候，还是拒绝 Scala 的，因为当时我看到的很多代码，用 Java 8 的 Lambda 和函数式都能解决。直到参与了使用 Scala 开发的 Mock Agent 之后才感受到 Scala 语言的灵活好用。函数式语言在写这种分析计算程序时，因为其语言本身的强大表达能力写起来真的很快。这也是为什么目前大数据框架，很多都是 Scala 编写的缘故。</p>
<p>Akka 的使用，我目前还只停留在表面，但是它提供的 Actor 模型，Actor Cluster 等，在分布式平台还是极其便捷的。</p>
<p>Antlr4 的学习，符号化与 SQL 生成。在编写 DSL 的时候，最大的感受就是解析与语言生成，它们正好是两个相反的过程。一个是将语言符号化解析成树，另一个是基于类似的定义生成语言。这一正一反的过程，在我们适配旧的告警规则配置数据的时候，感受颇深，十分奇妙。</p>
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/67.jpg?v=1777731766948" alt="" width="1366" height="768" data-thumbhash="/fcBBID2R3ebiYh7cYr3inyvuA==">
<img src="https://cat.yufan.me/images/slide/two-year-in-oneapm/68.jpg?v=1777731766980" alt="" width="1366" height="768" data-thumbhash="/fcBBIDJmaJqiIWPcqk/X9bABg==">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/study">学习</category>
            <category domain="https://yufan.me/tags/code">代码</category>
            <category domain="https://yufan.me/tags/summary">总结</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/two-years-in-oneapm.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Yandex.Metrica 的革命性的数据存储结构]]></title>
            <link>https://yufan.me/posts/evolution-of-data-structures-in-yandex-metrica</link>
            <guid isPermaLink="false">https://yufan.me/posts/evolution-of-data-structures-in-yandex-metrica</guid>
            <pubDate>Fri, 29 Sep 2017 18:51:53 GMT</pubDate>
            <description><![CDATA[Yandex.Metrica 是世界第二大的线上分析系统，Metrica 处理来自网站或者应用的数据流，将它解析成可分析的格式。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019051818284917.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019050908220042.png"><link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019050908223772.png"><link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019050908233838.gif"><img src="https://cat.yufan.me/images/2019/05/2019051818284917.jpg?v=1777738726110" alt="2017-10-03-metrica-600" width="2000" height="1000" data-thumbhash="8/YFHIq9Wmc735Wmpjz8F7P1dA==">
<p>Yandex.Metrica 是世界<a href="https://w3techs.com/technologies/overview/traffic_analysis/all" rel="nofollow" target="_blank">第二大</a>的线上分析系统，Metrica 处理来自网站或者应用的数据流，将它解析成可分析的格式。</p>
<p>Metrica 处理分析消息起来游刃有余，我们主要的技术挑战在于如何以最便于使用的格式来存储处理结果。在整个开发过程中，我们前后几次改变了存储方式。从最初的 MyISAM 表，到后面的 LSM 树，最后变成列式存储数据库 <a href="https://clickhouse.yandex/" rel="nofollow" target="_blank">ClickHouse</a>。我将在本文中详解为何我们最后选择自己开发了 ClickHouse。</p>
<p>Yandex.Metrica 从2008年公布至今已经平稳运行9年了，每次我们改变数据存储访问方式都是因为它不够高效。这当中有：数据写入性能不理想的原因；有存储不可靠的原因；有消耗过多计算资源的原因；或者仅仅是它不能按照我们的需求去扩展。</p>
<p>旧的 Yandex.Metrica 网站有超过40多种固定报表类型（例如：访客地域报告）；几个内页分析工具（例如：点击图）；网站访客（帮助你详细分析单独访客行为）。当然，Yandex.Metrica 还提供单独的报表构造器。</p>
<p>新的 Metrica 和 Appmetrica 系统允许你定制每种报表，而不是以前的“固定”类型查询。你可以增加新的<strong>查询维度</strong>（例如：搜索词条报告中，你可以增加访问页面进一步分析）、<strong>分段</strong>、<strong>比较</strong>（例如：所有的访客数据和旧金山的访客数据比较），改变你的<strong>查询指标集</strong>等。所以，新的系统依赖的数据存储方式和我们之前使用的完全不一样。</p>
<h2 id="myisam">MyISAM<a href="#myisam"><span class="icon icon-link"></span></a></h2>
<p>在设计初期，Metrica 被设计为 Yandex.Direct 的分支之一，搜索广告服务。因为 Direct 使用 MySQL 的 MyISAM 引擎，所以 Metrica 开发初期同样沿用了相同的存储。2008 年至 2011 年，MyISAM 引擎被用于存储**“固定”**的分析报表。</p>
<p>请让我以地域报表为例，介绍一下报表的数据结构。一份报表是对特定网站的数据汇总（更具体地说，是一个特定的Metrica计数标识），这代表着主键应该包含计数ID（<code>CounterID</code>）。因为用户可以选择任意报告周期，所以按照每个日期对方式存储数据毫无意义。因此，数据按照产生的日期去存储，然后通过查询的时间区间累积计算。综上所述，主键中包含日期。</p>
<p>报告中的数据，要么按照地域维度展示为列表，要么按照国家、地域、城市等维度展示为一棵树。所以，需要在主键中存放地域ID（<code>RegionID</code>），这样可以在应用代码中将数据聚合成一棵树，而不是依赖数据库层面的计算。</p>
<p>同时，我们需要计算平均会话周期。这意味着数据表中应该包含<strong>会话总数</strong>和<strong>总的会话时长</strong>。</p>
<p>按照上述需求，数据表的结构应该是： <code>CounterID</code>, <code>Date</code>, <code>RegionID -&gt; Visits</code>, <code>SumVisitTime</code>, ... 基于这种结构，我们在 <code>SELECT</code> 查询时，应该按照 <code>WHERE CounterID = AND Date BETWEEN min_date AND max_date</code> 的条件去查询结果。换言之，主键的范围是命中的。</p>
<h3 id="数据在磁盘上如何存储-how-is-data-actually-stored-on-the-disk">数据在磁盘上如何存储？ How is data actually stored on the disk?<a href="#数据在磁盘上如何存储-how-is-data-actually-stored-on-the-disk"><span class="icon icon-link"></span></a></h3>
<p>一张 MyISAM 引擎表由一个数据文件和一个索引文件组成。如果更新表的时候不删除数据且不改变长度，数据文件将按照插入的顺序存储每一行序列化的数据。索引（包含主键索引）是 B-树，叶子节点存储数据文件的位移。当我们读取一个索引中的范围数据时，首先从索引中查出一组满足查询条件的数据文件位移，然后按照查出来的位移依次去从数据文件中查找出实际的数据。</p>
<p>以索引存储于RAM（MySQL 的键缓存或者是系统页缓存）而数据没有被缓存的实际场景举例。如果使用磁盘，读取数据的时间取决于需要读取的数据量以及需要完成多少次检索操作，检索的次数基于在磁盘上存储的区域数量。</p>
<p>Metrica 事件基本按照它们生成的顺序接收处理，在收集端，数据从不同的计数器随机产生。换言之，数据存储时按照时间是连续的，但是按照生产者是不连续的。当写入至一张 MyISAM 表时，来自不同计数器的数据也是非常随机地存储的。这意味着生成报告的时候，需要几行数据，就可能需要执行同样次数的随机检索。</p>
<p>一块经典的 7200 转的硬盘可以每秒钟执行 100 ~ 200 次的随机读取。一个磁盘矩阵，如果使用得当，可以按照比例地执行更多次随机读取。一块使用7年的 SSD 每秒钟可执行 30000 多次的随机读取，但是我们无法支付将数据存放于 SSD 的硬件成本。在目前的系统中，如果我们在一份报表中需要读取10000行数据，大概需要10秒钟，这是完全无法接受的一个时长。</p>
<p>InnoDB 引擎更适合用于主键范围检索，因为它使用聚集主键（聚集索引？）。简单说，数据基于主键以有序的方式存储。但是 InnoDB 的写入速度慢到无法接受。如果你推荐 TokuDB，请继续阅读。</p>
<p>我们采取了一些措施来让 MyISAM 引擎在主键范围检索时更快。</p>
<p><strong>表排序</strong> 因为数据需要立即更新，对表只做一次排序是远远不够的，但是每次写入都去做排序也不现实。虽然我们可以定期对旧数据做排序。</p>
<p><strong>分区</strong> 一张表可以划分成许多小的主键范围，这么做的目的是为了让同一分区的数据存储得更加连续，基于主键范围的检索能更快。此方法可参考在聚集主键的上手动实现。虽然这么做会导致插入速度明显下降，但是通过控制分区的数量，我们可以在插入速度和检索速度中找到可接受的数值。</p>
<p><strong>按照数据的年龄分割</strong> 单一分区的检索会非常慢，分区多了，插入速度也会变慢。当在这当中取一个中间分区数时，插入和检索速度都不是最快的。对于此问题的解决方案就是将数据分成几个单独的代。举例来说，第一代我们叫做可操作数据，这是数据写入时，分区（按时间）或者不分区的地方。第二代我们叫做归档数据，这是随着数据检索（按照计数ID）进行分区的地方。数据通过脚本从一个代转移到另一个代，但不会特别频繁（例如：一天一次），并能在所有的代上立刻检索。这的确解决了问题，但是也增加了许多复杂性。</p>
<p>上面（还有一些别的未列举）就是 Yandex.Metrica 使用的优化策略。</p>
<p>让我们总结一下这套方案的缺点：</p>
<ul>
<li>无法支撑数据在磁盘上连续存储</li>
<li>在插入时表需要加锁</li>
<li>复制十分慢，副本经常有延迟</li>
<li>硬件故障后的数据一致性不能保证</li>
<li>诸如独立用户数量的聚合查询很难计算和存储</li>
<li>数据压缩很难实现并且不高效</li>
<li>索引很大，并且不能在内存中完全存储</li>
<li>许多计算需要在 <code>SELECT</code> 查询之后编程去计算</li>
<li>运维麻烦</li>
</ul>
<img src="https://cat.yufan.me/images/2019/05/2019050908220042.png?v=1777738726110" alt="2017-10-03-locality" width="600" height="189" data-thumbhash="LSkKGoRn+Hh4h7d3iYlwVg0=">
<p>图片：数据在磁盘上的存储区域（艺术渲染）</p>
<p>总而言之，MyISAM 引擎使用起来极不方便。每天的服务器磁盘阵列的负载都是满的（磁头一直在移动），这种情况下，磁盘故障频发。我们在服务器上使用了SAN存储，换言之，我们不得不频繁恢复RAID阵列。有时候，副本的延迟极高导致不得不删除重建，切换至复制主节点极其不便。</p>
<p>尽管 MyISAM 缺点多多，到了 2011 年，我们已经基于它存储了超过 5800 亿的数据。在那之后，所有的数据被转换至 Metrage，因此释放了很多服务器资源。</p>
<h2 id="metrage-and-olapserver">Metrage and OLAPServer<a href="#metrage-and-olapserver"><span class="icon icon-link"></span></a></h2>
<p>2010年后我们开始使用 Metrage 存储固定报表，假设你有下述场景：</p>
<ul>
<li>数据持续以小批次写入数据库</li>
<li>写入流相对比较大（每秒至少几十万行）</li>
<li>检索查询想多较少（每秒大概几千次查询）</li>
<li>所有的检索命中主键范围（每次查询高达100多万行）</li>
<li>每行数据相对较小（未压缩的数据大概100个字节）</li>
</ul>
<p>数据结构中的 LSM 树十分适合上述的业务需求，且比较常见。</p>
<hr>
<p>A fairly common data structure, LSM Tree, works well for this. This structure consists of a comparatively small group of data &quot;chunks&quot; on the disk, each of which contains data sorted by primary key. New data is initially placed in some type of RAM data structure (MemTable) and then written to the disk in a new, sorted chunk. Periodically a few sorted chunks will be compacted into one larger one in the background. This way a relatively small set of chunks are maintained.</p>
<p>This kind of data structures is used in HBase and Cassandra. Among embedded LSM-Tree data structures, LevelDB and RocksDB are implemented. Subsequently, RocksDB is used in MyRocks, MongoRocks, TiDB, CockroachDB and many others.</p>
<img src="https://cat.yufan.me/images/2019/05/2019050908223772.png?v=1777738726110" alt="lsm-tree-600" width="600" height="97" data-thumbhash="90gKGYqomXd4iPhnV4iAVQg=">
<p>Metrage is also an LSM-Tree. Arbitrary data structures (fixed at compile time) can be used as &quot;rows&quot; in it. Every row is a key, value pair. A key is a structure with comparison operations for equality and inequality. The value is an arbitrary structure with operations to update (to add something) and merge (to aggregate or combine with another value). In short, it&#x27;s a CRDT.</p>
<p>Both simple structures (integer tuples) and more complex ones (like hash tables for calculating the number of unique visitors or click-map structures) can serve as values. Using the update and merge operations, incremental data aggregation is constantly carried out at the following points:</p>
<ul>
<li>during data insertion when forming new batches in RAM</li>
<li>during background merges</li>
<li>during read requests</li>
</ul>
<p>Metrage also contains the domain-specific logic we need that&#x27;s performed during queries. For example, for region reports, the key in the table will contain the ID of the lowest region (city, village) and, if we need a country report, the country data will finish aggregating on the database server side.</p>
<p>Here are the main advantages of this data structure:</p>
<ul>
<li>Data is located pretty locally on the hard disk; reading the primary key range goes quickly.</li>
<li>Data is compressed in blocks. Because data is stored in an orderly manner, compression works pretty well when fast compression algorithms are used (in 2010 we used QuickLZ, since 2011 - LZ4).</li>
<li>Storing data sorted by primary key enables us to use a sparse index. A sparse index is an array of primary key values ​​for each Nth row (N-order of thousands). This index is maximally compact and always fits on the RAM.</li>
</ul>
<p>Since reading is not performed very often (even though lot of rows are read when it does) the increase in latency due to having many chunks and decompressing the data blocks does not matter. Reading extra rows because of the index sparsity also does not make a difference.</p>
<p>Written chunks of data are not modified. This allows you to read and write without locking - a snapshot of data is taken for reading. Simple and uniform code is used, but we can easily implement all the necessary domain-specific logic.</p>
<p>We had to write Metrage instead of amending an existing solution because there really wasn&#x27;t one. LevelDB did not exist in 2010 and TokuDB was proprietary at the time.</p>
<p>All systems that implement LSM-Tree were suitable for storing unstructured data and maps from BLOB to BLOB with slight variations. But to adapt this type of system to work with arbitrary CRDT would have taken much longer than to develop Metrage.</p>
<p>Converting data from MySQL to Metrage was rather time consuming: while it only took about a week for the conversion program to work, the main part of it took about two months to work out.</p>
<p>After transferring reports to Metrage, we immediately saw an increase in Metrica interface speed. We&#x27;ve been using Metrage for five years and it has proved to be a reliable solution. During that time, there were only a few minor failures. It&#x27;s advantages are its simplicity and effectiveness, which made it a far better choice for storing data than MyISAM.</p>
<p>As of 2015 we stored 3.37 trillion rows in Metrage and used 39 * 2 servers for this. Then we have moved away from storing data in Metrage and deleted most of the tables. The system has its drawbacks; it really only works effectively with fixed reports. Metrage aggregates data and saves aggregated data. But in order to do this, you have to list all the ways in which you want to aggregate data ahead of time. So if we do this in 40 different ways, it means that Metrica will contain 40 types of reports and no more.</p>
<p>To mitigate this we had to keep for a while a separate storage for custom report wizard, called OLAPServer. It is a simple and very limited implementation of a column-oriented database. It supports only one table set in compile time — a session table. Unlike Metrage, data is not updated in real-time, but rather a few times per day. The only data type supported is fixed-length numbers of 1-8 bytes, so it wasn’t suitable for reports with other kinds of data, for example URLs.</p>
<h2 id="clickhouse">ClickHouse<a href="#clickhouse"><span class="icon icon-link"></span></a></h2>
<p>Using OLAPServer, we developed an understanding of how well column-oriented DBMS&#x27;s handle ad-hoc analytics tasks with non-aggregated data. If you can retrieve any report from non-aggregated data, then it begs the question of whether data even needs to be aggregated in advance, as we did with Metrage database.</p>
<img src="https://cat.yufan.me/images/2019/05/2019050908233838.gif?v=1777738726110" alt="column-oriented-600" width="600" height="194" data-thumbhash="dCsKQoTgd2qHiLhpH4f0gUg=">
<p>On the one hand, pre-aggregating data can reduce the volume of data that is used at the moment when the report page is loading. On the other hand, though, aggregated data doesn&#x27;t solve everything. Here are the reasons why:</p>
<ul>
<li>you need to have a list of reports that your users need ahead of time</li>
<li>in other words, the user can&#x27;t put together a custom report</li>
<li>when aggregating a lot of keys, the amount of data is not reduced and aggregation is useless</li>
<li>when there are a lot of reports, there are too many aggregation options (combinatorial explosion)</li>
<li>when aggregating high cardinality keys (for example, URLs) the amount of data does not decrease by much (by less than half)</li>
<li>due to this, the amount of data may not be reduced, but actually grow during aggregation</li>
<li>users won&#x27;t view all the reports that we calculate for them (in other words, a lot of the calculations prove useless)</li>
<li>it&#x27;s difficult to maintain logical consistency when storing a large number of different aggregations</li>
</ul>
<p>As you can see, if nothing is aggregated and we work with non-aggregated data, then it&#x27;s possible that the volume of computations will even be reduced. But only working with non-aggregated data imposes very high demands on the effectiveness of the system that executes the queries.</p>
<p>So if we aggregate the data in advance, then we should do it constantly (in real time), but asynchronously with respect to user queries. We should really just aggregate the data in real time; a large portion of the report being received should consist of prepared data.</p>
<p>If data is not aggregated in advance, all the work has to be done at the moment the user request it (i.e. while they wait for the report page to load). This means that many billions of rows need to be processed in response to the user&#x27;s query; the quicker this can be done, the better.</p>
<p>For this you need a good column-oriented DBMS. The market didn’t have any column-oriented DBMS&#x27;s that would handle internet-analytics tasks on the scale of Runet (the Russian internet) well enough and would not be prohibitively expensive to license.</p>
<p>Recently, as an alternative to commercial column-oriented DBMS&#x27;s, solutions for efficient ad-hoc analytics of data in distributed computing systems began appearing: Cloudera Impala, Spark SQL, Presto, and Apache Drill. Although such systems can work effectively with queries for internal analytical tasks, it is difficult to imagine them as the backend for the web interface of an analytical system accessible to external users.</p>
<p>At Yandex, we developed and later <a href="https://github.com/yandex/ClickHouse/" rel="nofollow" target="_blank">opensourced</a> our own column-oriented DBMS — ClickHouse. Let&#x27;s review the basic requirements that we had in mind before we proceeded to development.</p>
<p>Ability to work with large datasets. In current Yandex.Metrica for websites, ClickHouse is used to store all data for reports. As of September, 2017, the database is comprised of 25.1 trillion rows. It’s made up of non-aggregated data that is used to retrieve reports in real-time. Every row in the largest table contains over 500 columns.</p>
<p>The system should scale linearly. ClickHouse allows you to increase the size of cluster by adding new servers as needed. For example, Yandex.Metrica&#x27;s main cluster has increased from 60 to 426 servers in three years. In the aim of fault tolerance, our servers are spread across different data centers. ClickHouse can use all hardware resources to process a single query. This way more than 2 terabyte can be processed per second.</p>
<p>High efficiency. We really focus on our database&#x27;s high performance. Based on the results of internal tests, ClickHouse processes queries faster than any other system we could acquire. For example, ClickHouse works an average of 2.8-3.4 times faster on web analytics queries than one of top performing commercial column-oriented DBMS (let&#x27;s call it DBMS-V).</p>
<p>Functionality should be sufficient for Web analytics tools. The database supports the SQL language dialect, subqueries and JOINs (local and distributed). There are numerous SQL extensions: functions for web analytics, arrays and nested data structures, higher-order functions, aggregate functions for approximate calculations using sketching, etc.</p>
<p>ClickHouse was initially developed by the Yandex.Metrica team. Furthermore, we were able to make the system flexible and extensible enough that it can be successfully used for different tasks. Although the database can run on large clusters, it can be installed on single server or even on a virtual machine.</p>
<p>ClickHouse is well equipped for creating all kinds of analytical tools. Just consider: if the system can handle the challenges of Yandex.Metrica, you can be sure that ClickHouse will cope with other tasks with a lot of performance headroom to spare.
ClickHouse works well as a time series database; at Yandex it is commonly used as the <a href="https://github.com/yandex/graphouse/" rel="nofollow" target="_blank">backend for Graphite</a> instead of Ceres/Whisper. This lets us work with more than a trillion metrics on a single server.</p>
<p>ClickHouse is used by analytics for internal tasks. Based on our experience at Yandex, ClickHouse performs at about three orders of magnitude higher than ancient methods of data processing (scripts on MapReduce). But this is not a simple quantitative difference. The fact of the matter is that by having such a high calculation speed, you can afford to employ radically different methods of problem solving.</p>
<p>If an analyst has to make a report and they are competent at their job, they won&#x27;t just go ahead and construct one report. Rather, they will start by retrieving dozens of other reports to better understand the nature of the data and test various hypotheses. It is often useful to look at data from different angles in order to posit and check new hypotheses, even if you don&#x27;t have a clear goal.</p>
<p>This is only possible if the data analysis speed allows you to conduct instant research. The faster queries are executed, the more hypotheses you can test. Working with ClickHouse, one even gets the sense that they are able to think faster.</p>
<p>In traditional systems, data is like a dead weight, figuratively speaking. You can manipulate it, but it takes a lot of time and is inconvenient. If your data is in ClickHouse though, it is much more malleable: you can study it in different cross-sections and drill down to the individual rows of data.</p>
<p>After one year of open source, ClickHouse is now used by hundreds of companies worldwide. For instance, <a href="https://blog.cloudflare.com/how-cloudflare-analyzes-1m-dns-queries-per-second/" rel="nofollow" target="_blank">CloudFlare</a> is using ClickHouse for analytics of DNS traffic, ingesting about 75 billion events each day. Another example is <a href="https://www.dropbox.com/s/l0qx4feez3kokd9/Go%20July%20meetup.%20Go%20-%20ClickHouse%20-%20Grafana.pdf?dl=0" rel="nofollow" target="_blank">Vertamedia</a> (a video SSP platform), which processes 200 billion events each day in ClickHouse with an ingestion rate of about 3 million rows per second.</p>
<h2 id="conclusions">Conclusions<a href="#conclusions"><span class="icon icon-link"></span></a></h2>
<p>Yandex.Metrica has become the second largest web-analytics system in the world. The volume of data that Metrica takes in grew from 200 million events a day in 2009 to more than 25 billion in 2017. In order to provide users with a wide variety of options while still keeping up with the increasing workload, we&#x27;ve had to constantly modify our approach to data storage.</p>
<p>Effective hardware utilization is very important to us. In our experience, when you have a large volume of data, it&#x27;s better not to worry as much about how well the system scales and instead focus on how effectively each unit of hardware is used: each processor core, disk and SSD, RAM, and network. After all, if your system is already using hundreds of servers, and you have to work ten times more efficiently, it is unlikely that you can just proceed to install thousands of servers, no matter how scalable your system is.</p>
<p>To maximize efficiency, it&#x27;s important to customize your solution to meet the needs of specific type of workload. There is no data structure that copes well with completely different scenarios. For example, it&#x27;s clear that key-value databases don&#x27;t work for analytical queries. The greater the load on the system, the narrower the specialization required. One should not be afraid to use completely different data structures for different tasks.</p>
<p>We were able to set things up so that <a href="https://metrica.yandex.com/" rel="nofollow" target="_blank">Yandex.Metrica</a>&#x27;s hardware was relatively inexpensive. This has allowed us to offer the service free of charge to even very large sites and mobile apps, even larger than Yandex’s own, while competitors typically start asking for a paid subscription plan.</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/database">数据库</category>
            <category domain="https://yufan.me/tags/storage">存储</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/evolution-of-data-structures-in-yandex-metrica.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Spring Boot 简介]]></title>
            <link>https://yufan.me/posts/springboot-brief-intro</link>
            <guid isPermaLink="false">https://yufan.me/posts/springboot-brief-intro</guid>
            <pubDate>Sat, 13 May 2017 04:51:59 GMT</pubDate>
            <description><![CDATA[第一次使用 Springboot 应该是15年年底，当时就被这种约定大于配置的设计惊呆了。那个时候才从上一家公司跳槽，用的是 Spring  3。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/01.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/02.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/03.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/04.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/05.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/06.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/07.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/08.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/09.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/10.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/11.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/12.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/13.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/14.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/15.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/16.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/17.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/18.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/19.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/20.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/21.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/22.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/23.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/24.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/25.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/26.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/27.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/28.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/29.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/30.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/31.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/32.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/slide/spring-boot-intro/33.jpg"><p>第一次使用 Springboot 应该是15年年底，当时就被这种约定大于配置的设计惊呆了。那个时候才从上一家公司跳槽，用的是 Spring 3。所以每次开发新项目的时候，配置起来都让我十分痛苦，也因此喜欢上了 Springboot 的种种便利。</p>
<p>众观 Springboot 的发展，可以发现，其在简化开发上不断地进步。很多常见的组件框架也有了 Springboot 版本。我想，作为 Java 程序员，是时候进入 Springboot 的世界了。</p>
<p>这里分享一份一年前，我在公司内部分享上用的 Slide，以期对于阅读此文的你有所帮助。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/01.jpg?v=1777731763543" alt="" width="2250" height="1688" data-thumbhash="o7gBBYAZd1igiIqBe5eXmeZvrgAZ">
<p>大家好，今天由我来向大家简单介绍一下 Spring Boot 相关的内容，详细的与 Spring Boot 相关的知识将后面由吴一敏同学分享。</p>
<p>首先自我介绍一下，我叫盛宇帆，15年年底加入 OneAPM，现在已经1年多了，目前主要负责和告警引擎相关的开发。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/02.jpg?v=1777731763508" alt="" width="2250" height="1688" data-thumbhash="OegJDYT3V4h7hod7g2mYdThweL/Y">
<p>Spring 官方的博客介绍 Spring： <strong>Spring</strong> is the <strong>“glue”</strong> in your application。我认为 Spring Boot 就是 A glue in Spring Framework。</p>
<p>相信大家都经历过配置 Spring XML 的阶段，十分痛苦地去配置一个 Bean，后面 Spring 3发布之后，基本上很多配置都是通过注解加扫包去配置初始化。
我常见到的一种配置方式，就是和 Spring 框架集成部分的配置，如数据库啊，Web 模板一类的，使用的是 XML，自己项目的 Service、Dao 等类，使用注解初始化。
后面 Spring 4开始流行 @Configuation 注解的配置类初始化配置。</p>
<p>然而，这样子还是十分麻烦，所以才有了 Spring Boot，它给我们最直观的感受，就是 简化了配置。一言一概之，约定大于配置。</p>
<p>然而，仅有这些，并不能说明为何现在 Spring Boot 开始流行，说道 Spring Boot 的兴起，我想起前几天一个技术群的提问：为什么 Spring Boot 应用倾向于打 fat jar 直接启动，而传统的应用倾向于打 war 包从应用容器启动？</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/03.jpg?v=1777731763573" alt="" width="2250" height="1688" data-thumbhash="ctgNDYTqp4V5h4dvhIeHeAR0oa74">
<p>Java 应用部署于应用容器中，其实是受到 J2EE 的影响，也算是 Java Web 有别于其他 Web 快速开发语言的一大特色。一个大大的 war 压缩包，包含了全部的依赖，代码，静态资源，模板。</p>
<p>在虚拟化流行之前，应用都是部署在物理机上的，为了节约成本，多 war 包部署在一个 Servlet 容器内。</p>
<p>但是为了部署方便，如使用的框架有漏洞、项目 jar包的升级，我们会以解压 war 包的方式去部署。或者是打一个不包含依赖的空 war 包，指定容器的加载某个目录，这样所有的war项目公用一套公共依赖，减少内存。当然缺点很明显，容易造成容器污染。</p>
<p>避免容器污染，多 war 部署变为多虚拟机单 war、单容器。</p>
<p>DevOps 流行，应用和容器不再分离，embedded servlet containers开始流行 Spring Boot 在这个阶段应运而生。于是项目部署变为 fat jar + 虚拟机</p>
<p>Docker的流行，开始推行不可变基础设施思想，实例（包括服务器、容器等各种软硬件）一旦创建之后便成为一种只读状态，不可对其进行任何更改。如果需要修改或升级某些实例，唯一的方式就是创建一批新的实例以替换。</p>
<p>基于此，我们将配置文件外置剥离，由专门的配置中心下发配置文件。</p>
<p>这也是我们为何要学习和使用 Spring Boot 的背景，我觉得这才是 Spring Boot 开始流行的主要原因。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/04.jpg?v=1777731763605" alt="" width="2250" height="1688" data-thumbhash="PfgFDYJwmJmOdoekeeholHhwf/gI">
<p>总的来说 Spring Boot 有以下几个特点。</p>
<ol>
<li>配置简化，这个我印象最深刻的就是写 MyBatis 的时候，一堆东西要配置，一般大家都会用那个 Generator 去生成。而实际上 Spring Boot 推崇 jpa，如果只是简单的 CRUD，用 Spring Boot + JPA 的方式简单到只需要几行关于数据库的配置就好了。</li>
<li>自动配置机制，很多教程都称它为 Magic，基于项目的某些条件，自动初始化装配必要的 Bean，稍后会在后面的演示中详解。</li>
<li>Starter 本质上就是 Spring 基于 Gradle 和 Maven 这两种构建工具定义的一组依赖，一般是按照功能或者框架划分。在有了自动配置的机制下，我们只需要依赖 Starter 指定的坐标，和非常简单的属性配置即可集成我们想要的框架。</li>
<li>嵌入的 Servlet 容器，主要是为了方便部署的。</li>
<li>主要是 <code>spring-boot-starter-actuator</code> 和 <code>spring-boot-starter-remote-shell</code> 的使用，当然，这里还可以使用 JMX 一类的做管理，大家可以参考文档。（2017年更新 remote shell 已经废弃）</li>
</ol>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/05.jpg?v=1777731763636" alt="" width="2250" height="1688" data-thumbhash="8PcNDYDnt3iEeYh/dPZXdqhyf6+Y">
<p>首先，我们来用 Spring Boot 写一个 Hello World吧，这个是仿照 Spring 官方的示例代码改的，使用 Groovy，所以连 import 都省了。这里主要是为了演示一个最简单的 Spring Boot 应用，通过下面的这行命令我们就能把它启动了。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">@</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RestController</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GreetingRestController</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    @</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RequestMapping</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;/hi/{name}&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    def </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">hi</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(@</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">PathVariable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        [ greeting</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;Hello, &quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/06.jpg?v=1777731763666" alt="" width="2250" height="1688" data-thumbhash="PvgFDYKXl3iOh3d0d9d4cHhwf/gI">
<p>现在，问题来了，刚才那个项目那么简单，那个，整个项目的启动过程中，到底发生了哪些魔法呢？</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/07.jpg?v=1777731763698" alt="" width="2250" height="1688" data-thumbhash="nOcVDYKYl5d4h3d/h4d3fIh1n/u5">
<p>我们将 Groovy 的代码翻译为 Java版本，大概会看到，一个项目想要启用 Spring Boot，关键在于 <code>SpringApplication</code> 类和 <code>EnableAutoConfiguration</code> 注解的使用。</p>
<p>SpringApplication 是 Spring Boot 提供的用于 Java main 方法的启动类。它的执行操作首先为：</p>
<ol>
<li>Create an appropriate ApplicationContext instance (depending on your classpath)</li>
<li>Register a CommandLinePropertySource to expose command line arguments as Spring properties</li>
<li>Refresh the application context, loading all singleton beans Trigger any CommandLineRunner beans</li>
</ol>
<p>然后 <code>EnableAutoConfiguration</code> 则为 Enable 类注解这里通过此注解，告诉 Spring Boot 开启自动装配的特性。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/08.jpg?v=1777731763728" alt="" width="2250" height="1688" data-thumbhash="PPgFDYJxqZmMh3eWaPdnhnhwf/gI">
<p>除了 <code>EnableAutoConfiguration</code>，我们常常和它并列使用的还有 <code>ComponentScan</code> <code>Configuration</code> 注解。这三个注解合起来，有一个等价的注解，叫做 <code>SpringBootApplication</code>，一般在我们的项目开发中，喜欢在项目最外面的包下面创建包含 main 方法的程序启动类，然后这个类上标记为 @SpringBootApplication 这个注解，这样就等价于基于 main 方法类所在的 package 为 Spring 扫包的基础包路径，且开启自动化配置。</p>
<p>自动化配置的实现，不得不说 Spring Boot 本质上是通过 Conditional 类注解来实现的。</p>
<p><code>@ConditionalOnClass</code> 表示对应的类在classpath目录下存在时，才会去执行注解所标示的自动配置类或者自动配置方法，与之对应的我们就@ConditionalOnMissingClass 注解，也就是找不到对应的类的时候。</p>
<p><code>@ConditionalOnBean</code> 和 <code>@ConditionalOnMissingBean</code> 则同样很容易按照字面意思理解。</p>
<p>当然 <code>Conditional*</code> 注解不仅仅上面提到的这些，还有 <code>ConditionalOnExpression</code> 一类的。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/09.jpg?v=1777731763769" alt="" width="2250" height="1688" data-thumbhash="nucZFYB4h4iFd3d/iIiHenh0r8r6">
<p>这个是我们从 Spring Boot 当中节选的一段代码，主要是为了演示自动化配置的具体实现。首先我们在项目配置里面标明 spring.jmx.enabled = true，ConditionalOnProperty 注解生效，然后 Spring 发现能找到 <code>MBeanExporter.class</code> 这个类，于是开始执行自动化配置的方法，因为这个时候项目中没有定义 <code>MBeanExporter</code> 这个 Bean，于是 <code>ConditionalOnMissingBean</code> 注解生效，Spring 开始读取配置属性，自动创建此 Bean 对象。</p>
<p>同理，任何一个自动装配的实现，基本上就是组合这些条件注解。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/10.jpg?v=1777731763799" alt="" width="2250" height="1688" data-thumbhash="PPgFDYKAiJiPh3aBiehodXhwf/gI">
<p>当然，如果能被 Spring Boot 官方直接支持的话是最吼的，目前被支持的肯定不止上面这些，我只是简单地列举了一些常见的项目。</p>
<p>Spring Boot 官方之前发起过好几次投票，就是列举一些框架，然后大家投票选择哪些想要被官方支持的。上半年的时候，我还在里面看到了之前姜老师维护的 camel，然而似乎并没有被选中。MyBatis 目前虽然有 Spring Boot 版，但是是由 MyBatis 团队自行维护，至少我6月份尝试使用的时候，问题还是蛮多的。 (2017年之后的版本基本可用，主要是有了 Boot 版本的 VFS)</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/11.jpg?v=1777731763829" alt="" width="2250" height="1688" data-thumbhash="H/gVBYKIh4eEeHh/d5d4eU+Qe/lJ">
<p>上面是我从 Spring Boot 1.3.6 中找到的 spring.factories 文件的截图。当然，基本上只要上面有的，都能得到不错的支持。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/12.jpg?v=1777731763859" alt="" width="2250" height="1688" data-thumbhash="PPgFDYJxqYl/hoiVaLeJdohyf/ko">
<p>前面我们简单介绍了自动配置的实现原理，基本上流程就是配置文件标明启用什么服务，然后找到对应的依赖（class），然后结合条件装配初始化 Bean。</p>
<p>所以 Spring 就更进一步，按照功能模块，划分出一个个 Starter 模块。以 Maven 为例，基本上我们只需要将 Spring Boot 自己的那个 POM 文件设置为 parent，然后依赖中直接依赖所需的 Starter 坐标，即可依赖所有所需的 jar 包，剩下的的东西，仅有最基础的属性值配置。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/13.jpg?v=1777731763896" alt="" width="2250" height="1688" data-thumbhash="3PcVDYJ3h3iHd3d/iIh4fIhzf/lI">
<p>当然 Starter 也是一把双刃剑，比如我在项目里面依赖了 <code>spring-boot-starter-data-jpa</code> 之后在 IDEA 里面看到的依赖树，简直就是 jar 包狂魔，虽然我们需要 jpa，但是不一定需要全部这些包。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/14.jpg?v=1777731763932" alt="" width="2250" height="1688" data-thumbhash="PPgFDYKVl4hoh4h3h/hodHhwf/gI">
<p>这也就引出了我使用 Starter 的时候的几个困扰。比如项目需要以 spring-boot 的 pom 为 parent，这个就比较讨厌了，尤其是我前公司，所有的项目是同一内部的 parent，这样子可以管理大家的依赖。如果要用Spring Boot 的话，就会略有麻烦。可能就需要通过依赖 Spring Boot 的 pom 的方式，并不是很优雅。</p>
<p>问题二是我在用 Spring Boot 时依赖 logstash 遇到的， logstash自己依赖了一个 logback-access 和那个版本的 Spring Boot 依赖的 logback 不一致，导致一直报一个 <code>java.lang.AssertionError</code></p>
<p>问题三就是我最近想用 Spring Boot 去读写 Kafka，结果我们用的 Kafka 版本比较老，最后只好自己配置，特别麻烦。很多老的组件，要么你得用老的 Spring Boot，要么你就得自己配置。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/15.jpg?v=1777731763965" alt="" width="2250" height="1688" data-thumbhash="IQgSBYDQpnmOlXiLhpiIh/6m+Lom">
<p>比如我们基于 YAML 定义了上图这么一段配置，最简单的方式就是 <code>@Value</code> 注解，通知这货还支持 Spring El 表达式，做一些简单的处理判断。</p>
<p>但是对于一个组件的配置，或者是项目自己的配置，更需要比较好的梳理，Spring Boot 便支持了所谓的 prefix 前缀的概念，我们可以把所需要的配置信息定义为一个配置类，在里面定义好必要的 Getter Setter 一类的东西。在初始化项目的时候使用 <code>@EnableConfigurationProperties</code> 注解即可实现配置参数注入到配置类里面。</p>
<p>当然麻烦的地方在于如果配置文件定义的层级过深，配置类会变得极其复杂。建议这种情况下，尽可能简化层级。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/16.jpg?v=1777731764002" alt="" width="2250" height="1688" data-thumbhash="+vcFBYKgqpZ/h4eFeOhXk6tGf/kY">
<p>配置文件的加载，其实 Spring Boot 有一个非常复杂的流程，大家好奇的话可以看 Spring Boot 文档中的定义，大概有十几种情况。但是大部分情况下，我们用不了这么多，上面是我认为应该知道并且利用的几种情况，配置加载的顺序是由上往下。</p>
<p>第一种情况，和 jar 包在同一目录下，一般是应用发布到生产，然后还想修改更新配置的情况。</p>
<p>项目 resources 目录和 resources/config 下面的配置文件，就是我们在开发的时候会选取的存放配置的位置。</p>
<p>当然配置文件的名称默认是 application，还可能根据你所启用的 Profile 加载不同名称的配置文件。</p>
<p>由于配置文件的指定在 Spring Boot 中极其灵活，（官方可能把所有的情况都考虑到了）所以大家可以自己按需选择。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/17.jpg?v=1777731764034" alt="" width="2250" height="1688" data-thumbhash="PPgFDYKTlpmLd4d2h/hmhXhwf/gI">
<p>测试当然也有 Starter，我们只需要依赖 <code>spring-boot-starter-test</code>，即可轻松写测试。常见的测试注解就是上面几个</p>
<p><code>@WebIntegrationTest</code> 注解相当于 <code>@IntegrationTest</code> + <code>@WebAppConfiguration</code> 注解结合使用，在 1.3 之前，主要是使用前面4个注解进行测试。</p>
<p>1.4 之后，我们主要使用 <code>SpringBootTest</code> 注解做测试</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/18.jpg?v=1777731764073" alt="" width="2250" height="1688" data-thumbhash="6PcNDYCTepemZ4iNd/iFr4aTT/ts">
<p>最老的方式，你可能会使用 <code>@ContextConfiguration</code> 注释和 <code>SpringApplicationContextLoader</code> 的组合去写单元测试。</p>
<p>当然，我们可以去掉 loader 的配置，使用方法2 的 <code>SpringApplicationConfiguration</code> 注解去测试</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/19.jpg?v=1777731764152" alt="" width="2250" height="1688" data-thumbhash="7/cRDYKQi5eJd4iqZ5d4q4KEL/tJ">
<p>当想写一个集成测试的时候，可以使用 <code>IntegrationTest</code> 注解，和前面的不一样的是，前面两种方式不会初始化全部的 Bean，而 <code>IntegrationTest</code> 会和生产环境一样，完整初始化程序。但是它不会初始化 嵌入式的 Servlet 容器。</p>
<p>当你需要嵌入式的 Servlet 容器做一些接口的集成测试的时候，就需要使用 <code>WebIntegrationTest</code> 注解</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/20.jpg?v=1777731764118" alt="" width="2250" height="1688" data-thumbhash="8vcRDYC3p5h1eIh/heZndJhzj/6p">
<p>然而一个项目，基本上包含 <code>SpringApplication</code> 和 main 方法的类只有一个，所以在1.4 之后，我们连 App.class 都不需要给定，直接使用 <code>@SpringBootTest</code> 注解即可，更加优雅。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/21.jpg?v=1777731764192" alt="" width="2250" height="1688" data-thumbhash="FPgNDYKId3eFiHd/iHh4fb21j/kZ">
<p>这个就是我基于 Spring Boot 1.3 写的一个集成测试，当然它使用的是我们前面说的方法3。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/22.jpg?v=1777731764225" alt="" width="2250" height="1688" data-thumbhash="PfgFDYKFh4d5iHd0iPhoc3hwf/gI">
<p>前面我们其实已经说到了 Spring Boot 的 <code>Profile</code> 可以让我们区分不同环境下加载的配置文件。比如开发环境，测试和线上，很多值都可以实现定制，而不需要重新打包项目。</p>
<p>使用 Profile 的第二个场景就是不同的 Profile 需要初始化不同的bean，比如以 DataSource 为例，测试的时候，因为测试环境不一样，我们更期望 DataSource 能用 H2一类的嵌入式数据库模拟。开发和生产环境，就需要初始化一个 MySQL 的 DataSource。当然我说的这个场景不需要我们专门去配置，因为 Spring Boot 已经替我们考虑到了这种情况，在需要 DataSource，但是没有这个 Bean，切依赖了 H2的 Driver 的时候，Spring 会自动创建一个 H2 的 DataSource。</p>
<p>还有一个我使用 Profile的场景就是 Swagger，它十分好用，尤其是开发的时候能基于注解自动生成 API Doc，和测试页面，然而，会存在的问题就是它有漏洞，我只希望在开发的时候启用 Swagger，这个时候就可以利用 Profile 来实现。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/23.jpg?v=1777731764259" alt="" width="2250" height="1688" data-thumbhash="PfgBDYKjqomPhniDZ8d2cXhwf/gI">
<p>Profile 可以启用一个或者多个，然而，也会导致一些问题，比如我们没有指定 Profile 的时候怎么办，或者我们有 profile 名为 mysql、cassadra。它们是相互 block 的，如何检查校验，避免冲突的 profile 同时启用呢？</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/24.jpg?v=1777731764294" alt="" width="2250" height="1688" data-thumbhash="HPgVDYKIh4h2h4d/eJiHe3qBf+kI">
<p>这个是添加默认 Profile 的方式，原来我是尝试在 application 配置文件里面设置，但是不生效，最后我只好手动编码实现。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/25.jpg?v=1777731764333" alt="" width="2250" height="1688" data-thumbhash="IPgdDYJ3h4eJh4h/iIh3enmAf/oo">
<p>这个方法是和 main 方法同级的一个方法，需要 Autowired Spring 的 Environment 接口，然后获取 Profile 的配置，即可自行实现判断逻辑。（期待后面 Profile 能更加完善，实现 Block 一类的属性）</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/26.jpg?v=1777731764363" alt="" width="2250" height="1688" data-thumbhash="OfgFDYKXh4h/h3d2d5h4e3hwfPhI">
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/27.jpg?v=1777731764398" alt="" width="2250" height="1688" data-thumbhash="O/gFDYKUp5h7hoiMh9d4YHhwf/gI">
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/28.jpg?v=1777731764430" alt="" width="2250" height="1688" data-thumbhash="PfgFDYJimJlrl4hxiPhocnhwf/gI">
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/29.jpg?v=1777731764462" alt="" width="2250" height="1688" data-thumbhash="JvgdDYCHh3d4h3d/iIh3dnlwfvkI">
<p>Spring Boot 官方推崇的部署方式是 jar，原因我们前面也分析过了，但是也会存在打包为 war 去部署的需求。这里我们只需要在和 Application.class 同级的路径下继承 SpringBootServletInitializer 类去定义一下 SpringApplicationBuilder 的配置即可，还是很轻松的。我这个截图的示例里面用了前面设置默认 Profile 的方法，重用了一下代码。</p>
<p>有了这么一个类之后，我们就可以在 pom 里面设置项目打包为 war，它既能 java –jar去执行这个 war，也能直接丢到 Tomcat 一类的容器里面运行。</p>
<p>我们在 17年的实践中发现，很多时候，非 Fatjar 也有一定的意义，于是有了下属的打包启动实践，大家可以去参考。</p>
<p><a href="https://gist.github.com/syhily/c66310c150653e8f92b9fa6693df8207" rel="nofollow" target="_blank">https://gist.github.com/syhily/c66310c150653e8f92b9fa6693df8207</a></p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/30.jpg?v=1777731764491" alt="" width="2250" height="1688" data-thumbhash="PfgFDYCUiId8h4eEd/hocYZwP/oY">
<p>如果想要快速创建一个 Spring Boot 项目开发，有且不仅有上述几种方式。</p>
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/31.jpg?v=1777731764522" alt="" width="2250" height="1688" data-thumbhash="PfgFDYJymJlrl3iTifdocnhwf/gI">
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/32.jpg?v=1777731764552" alt="" width="2250" height="1688" data-thumbhash="PPgBDYKQmohthYiUaPeJg3hwf/gI">
<img src="https://cat.yufan.me/images/slide/spring-boot-intro/33.jpg?v=1777731764684" alt="" width="2250" height="1688" data-thumbhash="eOgFDYK4R7eNd3hvdXpZd6ZgkvLq">
<p>你可以点击 <a href="https://cat.yufan.me/uploads/springboot-intro.pdf" rel="nofollow" target="_blank">这里</a> 下载到本地浏览。</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/spring">Spring</category>
            <category domain="https://yufan.me/tags/speech">演讲</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/springboot-brief-intro.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[在编程中思考，简化你的判断逻辑]]></title>
            <link>https://yufan.me/posts/rewrite-your-logic</link>
            <guid isPermaLink="false">https://yufan.me/posts/rewrite-your-logic</guid>
            <pubDate>Mon, 06 Mar 2017 04:31:33 GMT</pubDate>
            <description><![CDATA[之前看 Linus Toward 在去年的某次采访中说到的好代码坏代码，当中提到了逻辑的精简，能用更通用的逻辑减少 if else 的判断在某种程度上可以使你的代码变得更好。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2019/05/2019051818210131.jpg"><img src="https://cat.yufan.me/images/2019/05/2019051818210131.jpg?v=1777738726110" alt="在编程中思考，简化你的判断逻辑" width="1363" height="881" data-thumbhash="q+cJDIJ6iIl/dnd4iJeJj4v3iQ==">
<p>之前看 Linus Toward 在去年的<a href="https://www.youtube.com/watch?v=o8NPllzkFhE&amp;feature=youtu.be&amp;t=890" rel="nofollow" target="_blank">某次采访</a>中说到的好代码坏代码，当中提到了逻辑的精简，能用更通用的逻辑减少 if else 的判断在某种程度上可以使你的代码变得更好。最近一段时间重构了部分老代码，也 Review 了不少代码，对此观点深有感触。</p>
<p>很多时候，程序员接到的需求，产品巴不得你立刻就能搞定，有时候会给非常紧迫的时间点。这种情况下会带来的最直接问题，就是<a href="http://www.ituring.com.cn/article/263057" rel="nofollow" target="_blank">“设计坏味”</a>。有整体的架构设计的不合理，也有代码逻辑的问题。尤其是对于边界条件的处理，因为需求的急，很多时候大家就会按照业务语言去写。</p>
<p>比如，下面这个例子：</p>
<p>某一报警系统产生了报警邮件，现在需要按照类型显示不同的内容。</p>
<table><thead><tr><th>类型</th><th>报警选择对象</th><th>邮件中展示内容</th></tr></thead><tbody><tr><td>Web事务</td><td>单条Web事务</td><td>Tier，Web事务，节点</td></tr><tr><td>Web事务</td><td>Tier</td><td>Tier，节点</td></tr><tr><td>节点</td><td>某一个节点</td><td>节点</td></tr><tr><td>节点</td><td>Tier</td><td>Tier，节点</td></tr></tbody></table>
<p>如果按照业务的语言，我们可能写出如下的伪代码：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Web事务</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 单条Web事务) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,Web事务,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Web事务</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (节点</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 某一个节点) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (节点</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>可能你看到这里会立刻笑出来，哪有只写 <code>if</code> 不写 <code>else</code> 的。那好，也许你会写出这样的代码。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Web事务) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (单条Web事务) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,Web事务,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Tier) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (节点) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (某一个节点) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Tier) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Tier,节点;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>我相信，大部分人都能将判断逻辑写到这一层，但是，这就完了么？也许你会说完了，逻辑也正确，看起来也很清晰。然而，这远远不够。</p>
<p>其实我们可以发现，在每种情况下，都会返回 <code>节点</code> 信息。只返回节点信息的只有一种情况，其他的情况下，基本都返回 <code>Tier</code> 信息。只有 <code>Web事务 &amp;&amp; 单条Web事务</code> 的情况需要返回 <code>Web事务</code> 信息。所以，最后我们可以精简为两个判断。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">result </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;节点&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Web事务</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 单条Web事务) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    result </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;Web事务&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (Web事务 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">某一个节点) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    result </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;Tier&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span></code></pre>
<p>回到最初的命题，为何不要用业务的语言来编写判断逻辑呢？因为业务语言是给用户和产品看的，他们在描述上本身就不够精简。其次，业务的描述，很多时候，是定义边界，说明问题，而不是告诉你判断逻辑。</p>
<p>所以，在写代码的时候，更多的时候要细化逻辑。这样，在维护修改时，才更为方便。下面我举另一个更具体的例子。</p>
<p>这是我 Review Scala 代码的时候遇到的一个问题，首先我先用业务语言描述一下需求。需要判断某个规则的开闭状态，在一天的几点到几点间启用，且还可以额外设置是周一到周日的哪几天启用。</p>
<p>于是我看到当时的同事，写一个方法 <code>isSuppressTime</code> ，会给两个参数，第一个参数为一个时间戳 <code>timestamp</code> ，第二个参数为一个 <code>List[Map[String, String]]</code> 。</p>
<p><code>Map[String, String]</code> 里面有4个值，分别是：</p>
<ol>
<li>startTime，从零点到某个具体开始时间的秒数，0~86399。</li>
<li>endTime，从零点到某个具体结束时间的秒数，0~86399。</li>
<li>isDaily，当时间戳在 startTime endTime 之间时，如果此项为 true，则返回 true。</li>
<li>weekdays，周一到周日，1~7，以<code>,</code>间隔组成的字符串，如<code>1,4,5</code>。表示周一、周四、周五且时间戳在 startTime endTime 之间时返回 true</li>
</ol>
<p>最后他写出了如下的代码（Scala）：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isSuppressTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> System</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.currentTimeMillis(),</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">                   suppressTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rule.getSuppressTime)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(suppressTimes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  import</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> scala</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">collection</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">JavaConversions</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  suppressTimes.foreach(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.size </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> startTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;startTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> endTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;endTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> isDaily</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;isDaily&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekdays</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;weekday&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).split(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;,&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toList</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> zero</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zeroTimestamp()</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      //今天零点零分零秒的毫秒数</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> startTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> endTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (start </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> end </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isDaily.toBoolean)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> cal</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getInstance()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        cal.setTimeInMillis(now)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> day</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cal.get(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DAY_OF_WEEK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        else</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekday</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day.toString</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (weekdays.contains(weekday))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> zeroTimestamp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> cal</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getInstance()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cal.set(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">HOUR_OF_DAY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cal.set(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">SECOND</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cal.set(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">MINUTE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cal.set(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">MILLISECOND</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cal.getTimeInMillis()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>这个代码看着很长，判断很多，而且还用了超级多陈旧的 API，使得它的性能也不好。而且这是一段 Scala 的代码，却用了较多的 <code>return</code> 和 <code>var</code> ，这两个都是 Scala 不提倡的。最关键的，明明是函数式的代码，他却写出了过程式的感觉。给后面的维护人员（我）带来了不少困扰。</p>
<p>下面，我们来一点点优化这段代码（需要一点点 Java 功底），首先对于方法 <code>zeroTimestamp()</code> ，我第一眼看到的时候，是惊讶的，原作者用了一个比较旧的 <code>Calendar</code> 。对它最深刻的印象就是当年写 <code>SimpleDataFormat</code> 的时候，因为 <code>Calendar</code> 这货导致线程不安全。而每次创建 <code>SimpleDataFormat</code> 的开销比较大，最后不得不写了个 <code>ThreadLocal&lt;simpledataformat&gt;</code> 。</p>
<p>而Java8以后，我们可以直接用 <code>java.time</code> 下面的类来改写 <code>zeroTimestamp()</code> ，这里只需要一行代码，如下：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> zeroTimestamp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  Timestamp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.valueOf(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LocalDate</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.now().atStartOfDay()).getTime</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>回到 <code>isSuppressTime</code> 方法上来，我姑且不说他的设计多么麻烦，因为这是一个已经被广泛使用的方法，我能做到的就是在不改变签名的情况写来优化实现。</p>
<p>首先，开头我们就看到</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(suppressTimes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span></span></code></pre>
<p>这个空的判断和强制 <code>return</code> ，在 Java 里面， <code>null</code> 是一个很痛苦的事情，Scala 因为基于 JVM 也不例外，但是 Scala 和 Java 8 都分别有一个 <code>Option</code> 类（Java8是 <code>Optional</code> ），来做空值处理。</p>
<p>所以，我们这里第一时间可以干掉这个判断，写成如下方式</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes).map(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ba la ba la</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}).getOrElse(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<p>代码里面的 <code>times</code> 为方法中非空的 <code>suppressTimes</code> ，注释部分为核心的处理逻辑，这样我们避免了一个 <code>if</code> 判断，减少了一个 <code>return</code> 。</p>
<p>而其实， <code>Option([A]).map([B] =&gt; Boolean).getOrElse(false)</code> 等价于 <code>Option</code> 的 <code>exists</code> 方法，最后完整的代码应该如下：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isSuppressTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> System</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.currentTimeMillis(),</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">        suppressTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]])</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes).exists(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    times.foreach(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.size </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> startTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;startTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> endTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;endTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> isDaily</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;isDaily&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekdays</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;weekday&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).split(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;,&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toList</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> zero</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zeroTimestamp()</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        //今天零点零分零秒的毫秒数</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> startTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> endTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (start </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> end </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isDaily.toBoolean)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> cal</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getInstance()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          cal.setTimeInMillis(now)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          var</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> day</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cal.get(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DAY_OF_WEEK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          else</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekday</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day.toString</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (weekdays.contains(weekday))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    false</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>在上面的例子里面，我们已经干掉了两个 <code>return</code> 和一个 <code>if</code> ，让它有一点 Functional 的感觉了。</p>
<p>上面重构后代码中， <code>times</code> 的类型是 <code>util.List[util.Map[String, String]]</code> 。 <code>times.foreach(params =&gt; {})</code> 里的 <code>params</code> 对应的是 <code>util.Map[String, String]</code> 类型。所以，我们看到判断 <code>if (params != null &amp;&amp; params.size &gt; 0)</code> 时应该会发现，这就是一个list filter的过程嘛。 <code>.filter()</code> 之后不就是一个 <code>.map()</code> ，于是我们可以这么改：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes).exists(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  times.filter(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">params.isEmpty).map(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // ba la ba la</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }).contains(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<p>当然 <code>.map(xxx =&gt; Boolean).contains(true)</code> 等价于 <code>.exists()</code> ，于是我们重构的方法如下：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isSuppressTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> System</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.currentTimeMillis(),</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">        suppressTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]])</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes).exists(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    times.filter(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">params.isEmpty).exists(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> startTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;startTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> endTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;endTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> isDaily</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;isDaily&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekdays</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;weekday&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).split(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;,&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toList</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> zero</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zeroTimestamp()</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      //今天零点零分零秒的毫秒数</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> startTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> endTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (start </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> end </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isDaily.toBoolean)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> cal</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getInstance()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        cal.setTimeInMillis(now)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> day</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cal.get(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DAY_OF_WEEK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        else</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekday</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day.toString</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (weekdays.contains(weekday))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      false</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>这次重构，我们又去掉了一层 if 判断，改为 filter 实现，减少了一次返回。到了这一步，我们会发现，下面需要实现的就是一个方法，对 <code>util.Map[String, String]</code> 去做处理，返回一个布尔值，于是我们精简方法的实现代码为两部分：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isSuppressTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> System</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.currentTimeMillis(),</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">                   suppressTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rule.getSuppressTime)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .exists(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> times.filter(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">params.isEmpty).exists(isValidateTimes(_, now)))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>这个为边界过滤的方法。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isValidateTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">params</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> startTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;startTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> endTime</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;endTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> isDaily</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;isDaily&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekdays</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;weekday&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).split(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;,&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toList</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> zero</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zeroTimestamp()</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  //今天零点零分零秒的毫秒数</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> startTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> endTime.toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (start </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> end </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isDaily.toBoolean)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> cal</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getInstance()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cal.setTimeInMillis(now)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> day</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cal.get(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Calendar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DAY_OF_WEEK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    else</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekday</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> day.toString</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (weekdays.contains(weekday))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  false</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>这个为我们要重构的核心处理逻辑。我们优化整理它的判断条件，最后可以实现如下的完整代码：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isSuppressTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> System</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.currentTimeMillis(),</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">                   suppressTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rule.getSuppressTime)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  Option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">List</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]]](suppressTimes)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .exists(times </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> times.filter(params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">params.isEmpty)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .exists(isValidateTimes(_, now)))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> isValidateTimes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">params</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Boolean</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> zero</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zeroTimestamp()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;startTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> zero </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;endTime&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toLong </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> isDaily</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;isDaily&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toBoolean</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekdays</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.get(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;weekday&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).split(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;,&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).toList</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weekday</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> LocalDate</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.now().getDayOfWeek.getValue.toString</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  (start </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> end </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> now)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isDaily </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> weekdays.contains(weekday))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>这样，我们就实现了一个 <code>if</code> 都没有，一个 <code>return</code> 都没有的纯函数式写法。</p>
<p>总的来说，代码谁都能写出来，但是把需求从文字或者是流程描述换成编码实现时就有了对程序员抽象逻辑能力的需求。如何组织抽象，就像是如何写作文，或者是 Kata（空手道里面的招数、套路），不要按照业务描述写 if else，而要尽可能简化找到一致性的简单逻辑描述。</p>
<p>如果能将所有的特殊情况变为通用情况，简化逻辑判断，那么代码在后面的迭代中也会比较易于维护。</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/thinking">思考</category>
            <category domain="https://yufan.me/tags/encoding">编码</category>
            <category domain="https://yufan.me/tags/programming">编程</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/rewrite-your-logic.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[A scala exercise a day 1]]></title>
            <link>https://yufan.me/posts/a-scala-exercise-a-day-1</link>
            <guid isPermaLink="false">https://yufan.me/posts/a-scala-exercise-a-day-1</guid>
            <pubDate>Wed, 16 Nov 2016 02:44:41 GMT</pubDate>
            <description><![CDATA[编写一段代码，将 a 设置为一个 n 个随机整数的数组，要求随机数介于 [0, n) 之间。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090412291981.jpg"><img src="https://cat.yufan.me/images/2020/09/2020090412291981.jpg?v=1777738726110" alt="mocha ドラム水槽" width="1333" height="943" data-thumbhash="3hgGDYYsebmfy2i6h+lqZQjRYhBL">
<p>1、编写一段代码，将 a 设置为一个 n 个随机整数的数组，要求随机数介于 <code>[0, n)</code> 之间。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> randomArray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">n</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> until n) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">yield</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (math.random </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> n).toInt).toArray</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>2、编写一个循环，将整数数组中相邻的元素置换。例如，<code>Array(1, 2, 3, 4, 5)</code> 经过置换之后变为 <code>Array(2, 1, 4, 3, 5)</code>。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transferArray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.indices </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">%</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> temp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(i)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    array(i) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    array(i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> temp</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  array</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>3、重复前一个练习，不过这次生成一个新的值交换过的数组。用 <code>for/yield</code>。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transferArray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.indices) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">yield</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">%</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> match</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      case</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      case</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =&gt;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ==</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.length) array(i) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }).toArray</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>4、给定一个整数数组，产出一个新的数组，包含元素组中的所有正值，以原有顺序排列，之后的元素是所有的零或负值，以原有顺序排列。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sortArray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  val</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (left, right) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.partition(_ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&gt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  left </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> right</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>5、如何计算 <code>Array[Double]</code> 的平均值？</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> arrayAverage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Double</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Double</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  array.sum </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.length</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>6、如何重新组织 <code>Array[Int]</code> 的元素将它们以反序排列？对于 <code>ArrayBuffer[Int]</code> 你又会怎么做呢？</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> reverseAverage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.indices </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array.length </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> temp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(i)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    array(i) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> array(array.length </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    array(array.length </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> temp</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  array</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>对于 <code>ArrayBuffer</code> 如下</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> reverseAverage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ArrayBuffer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ArrayBuffer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  array.reverse</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>7、编写一段代码，产出数组中的所有值，去掉重复项。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> distinctAverage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Int</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  array.distinct</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>8、创建一个由 <code>java.util.TimeZone.getAvailableIDs</code> 返回的时区集合，判断条件是它们在美洲。去掉 <code>&quot;America/&quot;</code> 前缀并排序。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> sortedAmericanZone</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> java.util.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">TimeZone</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getAvailableIDs</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .filter(_.startsWith(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;America&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .map(_.replaceFirst(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;America/&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .sorted</span></span></code></pre>
<p>9、引入 <code>java.awt.datatransfer._</code> 并构建一个类型为 <code>SystemFlavorMap</code> 类型的对象： <code>val flavors = SystemFlavorMap.getDefaultFlavorMap().asInstanceOf[SystemFlavorMap]</code> 然后以 <code>DataFlavor.imageFlavor</code> 为参数调用 <code>getNativesForFlavor</code> 方法，以 Scala 缓冲保存返回值。</p>
<p>首先导入包</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> java</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">awt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">datatransfer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> scala</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">collection</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">JavaConversions</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> scala</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">collection</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mutable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Buffer</span></span></code></pre>
<p>然后编码</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-scala"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> flavors</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SystemFlavorMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.getDefaultFlavorMap().</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">asInstanceOf</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">SystemFlavorMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">val</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> flavor</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Buffer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> flavors.getNativesForFlavor(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DataFlavor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.imageFlavor)</span></span></code></pre>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/scala">Scala</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/a-scala-exercise-a-day-1.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Webpack 介绍：第一部分]]></title>
            <link>https://yufan.me/posts/introduction-to-webpack-part-1</link>
            <guid isPermaLink="false">https://yufan.me/posts/introduction-to-webpack-part-1</guid>
            <pubDate>Thu, 05 May 2016 10:15:29 GMT</pubDate>
            <description><![CDATA[Webpack 是近段时间非常流行的前端流程处理工具，用于实时执行构建任务和预处理你的文件。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090419305167.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090419235130.png"><link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090419234660.jpg"><img src="https://cat.yufan.me/images/2020/09/2020090419305167.jpg?v=1777738726110" alt="せばすちゃ - 座って聴く東方文化帳　CDジャケットイラスト" width="1200" height="700" data-thumbhash="2vgNFIB3aHqKiHeAeHRXigDyqA==">
<p>Webpack 是近段时间非常流行的前端流程处理工具，用于实时执行构建任务和预处理你的文件。</p>
<p>你也许会使用 <a href="http://gruntjs.com/" rel="nofollow" target="_blank">Grunt</a> 或者 <a href="http://gulpjs.com/" rel="nofollow" target="_blank">Gulp</a> 来做类似的事情。首先建立一个编译链，然后在上面定义从何处读取代码，将压缩处理好的 CSS 和 JavaScript 等静态资源输出到什么地方。</p>
<p>这些工具都非常流行和好用，然而我却要向你安利另一种实现此类需求的方法，那就是使用 <a href="https://webpack.github.io/" rel="nofollow" target="_blank">Webpack</a>。<em>新技能Get！</em></p>
<h2 id="什么是-webpack">什么是 Webpack？<a href="#什么是-webpack"><span class="icon icon-link"></span></a></h2>
<img src="https://cat.yufan.me/images/2020/09/2020090419235130.png?v=1777731744808" alt="" width="800" height="400" data-thumbhash="X/eFC4Aj9Dh7eaX2d2l/l4aXeoiQOIg=">
<p>Webpack 常被人们定义为“模块打包工具”（module bundler），它读取 JavaScript 模块，分析它们之间的依赖关系，然后用尽可能高效的方式将它们组织在一起，最后生成一个独立的 JS 文件。似乎看起来并没有什么牛逼的技术，像 <a href="http://requirejs.org/" rel="nofollow" target="_blank">RequireJS</a> 在多少年前就能实现相似的功能了。</p>
<p>当然，如果是这样子我就没必要安利你了，相比 RequireJS 之流它还是有自己的特色的。Webpack 能读取的不光是原生的 JavaScript 文件，模块加载器的设计使得它能支持更丰富的格式。</p>
<p>例如，它能分析出你的 JavaScript 模块需要一个 CSS 文件，甚至能分析出这个 CSS 文件需要的图片资源。然后，处理过的资源文件只包含最精简的必须文件。不信？让我们现在来实战体验。</p>
<h2 id="安装">安装<a href="#安装"><span class="icon icon-link"></span></a></h2>
<p>首先必须要安装的是 <a href="https://nodejs.org/en/" rel="nofollow" target="_blank">Node.js</a>，在这里我们假定你已经正确安装并且配置完毕。那么安装 Webpack 所需要做的事，就只剩下输入下面的这条命令：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> webpack</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -g</span></span></code></pre>
<p>这条命令将全局安装 Webpack，并能在系统的任何路径下执行 <code>webpack</code> 命令。下面我们新建一个文件夹，在里面新建一个基本的 HTML 文件，名为<code>index.html</code>，内容如下：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-html"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&lt;!</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">doctype</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> charset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;utf-8&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;Webpack fun&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;bundle.js&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span></code></pre>
<p>需要注意的是，这里定义的<code>bundle.js</code>暂时还不存在，稍后将由 Webpack 帮我们创建。另外，那个空的 H2 标签稍后我们将会使用到。</p>
<p>接下来，在上面文件夹里创建两个 JS 文件，分别叫做：<code>main.js</code>、<code>say-hello.js</code>。<code>main.js</code>你可以理解为 main 方法，也就是我们代码主要的执行入口。<code>say-hello.js</code>是一个简单的模块，它接收一个人名和 DOM 元素，然后在这个 DOM 元素上显示一条包含人名的欢迎信息。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-javascript"><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// say-hello.js</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">element</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  element.textContent </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &#x27;Hello &#x27;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &#x27;!&#x27;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>定义完 <code>say-hello.js</code> 这个模块后，我们在 <code>main.js</code> 里引用它，引用方法十分简单，只需要下面这两行代码：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-javascript"><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// main.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sayHello </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;./say-hello&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sayHello</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;Guybrush&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">querySelector</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;h2&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span></code></pre>
<p>如果现在我们打开前面创建的那个 HTML 文件，你们发现页面上没有显示任何内容。因为我们既没有引用<code>main.js</code>，也没有将其处理成浏览器可执行的代码。接下来，我们使用 Webpack 读取<code>main.js</code>。如果能成功分析它的依赖，将会创建一个名为<code>bundle.js</code>的文件，并能在浏览器中执行。</p>
<p>回到命令行里执行 Webpack，只需简单输入如下命令：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">webpack</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> main.js</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bundle.js</span></span></code></pre>
<p>第一个参数定义了 Webpack 分析依赖的起始文件。首先，它查看起始文件里是否定义了相关的依赖。如果有，它将读入依赖的文件，看看这个文件是否也有其他的依赖。通过这种方式，递归读取完整个程式依赖的全部文件。一旦阅读完毕，它将整个依赖打包为一个文件，名为 <code>bundle.js</code>。</p>
<p>在这个例子里，当你按下回车后，你会看到类似下面的输出：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Hash:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 3d7d7339a68244b03c68</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Version:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> webpack</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1.12.12</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Time:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 55ms</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    Asset</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     Size</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  Chunks</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">             Chunk</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Names</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bundle.js</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  1.65</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> kB</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">       0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  [emitted]  main</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">   [0] </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">./main.js</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 90</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bytes</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> {</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [built]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">   [1] </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">./say-hello.js</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 94</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bytes</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> {</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [built]</span></span></code></pre>
<p>现在，打开<code>index.html</code>，浏览器将会显示<code>Hello Guybrush!</code></p>
<h2 id="配置">配置<a href="#配置"><span class="icon icon-link"></span></a></h2>
<p>如果每次运行 Webpack 都要指定输入和输出文件的话就太让人讨厌了。当然，开发者早就替我们想好了。其实和 <code>Gulp</code>、<code>Grunt</code>类似，Webpack 需要在我们的项目根目录下创建一个名为<code>webpack.config.js</code>的文件，就可以简化大量重复的命令参数。</p>
<p>在本例中，内容如下：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-javascript"><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  entry: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;./main.js&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  output: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    filename: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;bundle.js&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>现在，我们只需要输入<code>webpack</code>这个命令，就能实现和之前一样的操作。</p>
<h2 id="开发服务器">开发服务器<a href="#开发服务器"><span class="icon icon-link"></span></a></h2>
<p>首先提个问题：每次你做了一些改动，如果都要手动去执行<code>webpack</code>命令来看结果的话，是不是特傻逼？要知道，<code>Gulp</code>在很早之前就支持定义<code>watch</code>这种监视文件修改的任务了。所以，Webpack也不例外，甚至它还更进一步，提供了一个基于Node.js Express框架的开发服务器。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> webpack-dev-server</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -g</span></span></code></pre>
<p>首先运行上面的命令安装开发服务器，然后运行命令<code>webpack-dev-server</code>。这个命令将会启动一个简单的 Web 服务器，以命令执行的路径为静态资源根目录。下面我们打开浏览器，输入<a href="http://localhost:8080/webpack-dev-server/" rel="nofollow" target="_blank">http://localhost:8080/webpack-dev-server/</a>。如果一切正常，你将看到类似下面的内容：</p>
<img src="https://cat.yufan.me/images/2020/09/2020090419234660.jpg?v=1777731744770" alt="" width="350" height="156" data-thumbhash="9PcNA4CHiHeAiJeIiYB7B6c=">
<p>现在，我们不仅有了一个超赞的轻量级 Web 服务器，我们还有了一个孜孜不倦地监听代码变更的观察者。如果 Webpack 发现我们修改了一个文件，它会自动运行 <code>webpack</code> 命令打包我们的代码并刷新页面。</p>
<p>假想一下，我们可以双屏写代码，一个屏幕放浏览器，一个屏幕开编辑器。浏览器实时刷新结果，无需我们做任何配置和操作，是不是很酷？</p>
<p>现在你可以自己感受一下：修改<code>main.js</code>里面传给<code>sayHello</code>方法的姓名参数，然后保存文件，看看浏览器里面的实时变化。</p>
<h2 id="加载器loaders">加载器（Loaders）<a href="#加载器loaders"><span class="icon icon-link"></span></a></h2>
<p>对于 Webpack 而言，最重要的特性就是<a href="https://webpack.github.io/docs/loaders.html" rel="nofollow" target="_blank">加载器</a>。加载器和<code>Gulp</code> <code>Grunt</code>上的“任务”（tasks）类似。基本上都是读取文件，然后通过某种方式处理文件，最后打包为我们所需的代码。</p>
<p>本文中，我们想在代码中用一些<a href="http://www.ecma-international.org/ecma-262/6.0/" rel="nofollow" target="_blank">ES2015</a>的语法。因为 ES2015 是当前最新的 JavaScript 版本，所以并没有被所有的浏览器支持。可是淫家就想写最新的代码装逼怎么办？那只好先写，写完后将 ES2016 版本的代码转换为老的 ES5 代码。</p>
<p>为了实现这个需求，我们需要使用当下最流行的 <a href="https://github.com/babel/babel-loader" rel="nofollow" target="_blank">Babel Loader</a> 来进行转换。根据官网的教程，我们使用下面的命令进行安装：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> babel-loader</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> babel-core</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> babel-preset-es2015</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --save-dev</span></span></code></pre>
<p>这条命令不仅安装了 Babel 加载器，还包含了它支持 ES2015 时所需要的依赖。</p>
<p>安装完加载器，我们需要告诉 Webpack 使用什么加载器，参考下面的实例更新 <code>webpack.config.js</code> 文件：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-javascript"><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  entry: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;./main.js&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  output: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    filename: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;bundle.js&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  module: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    loaders: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        test:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">js</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        exclude:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">node_modules</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        loader: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;babel&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        query: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          presets: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;es2015&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>在这个配置示例里，我们需要注意几个地方。首先，<code>test: /\.js$/</code>这行是一个正则表达式，表示文件名满足此正则表达式的文件将会被此加载器处理。这里，我们的定义是全部的JS文件。类似的，<code>exclude: /node_modules/</code>则是告诉 Webpack 忽略<code>node_modules</code>文件夹。<code>loader</code>和<code>query</code>我觉得十分好理解，就是使用Babel loader加载器处理ES2015语法的文件。</p>
<p>重启开发服务器，在命令行里按下<code>ctrl+c</code>，重新输入<code>webpack-dev-server</code>。现在我们来测试一下 ES6 的代码是否能被正确翻译呢？不如试试看将<code>sayHello</code>变量定义为一个常量。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-javascript"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> sayHello</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&#x27;./say-hello&#x27;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<p>保存后，Webpack应该自动重新编译我们的代码并刷新浏览器，你会发现代码正常执行，什么都没有变。我们用编辑器打开<code>bundle.js</code>文件，你会发现没有<code>const</code>这个单词。</p>
<p>Webpack就是这么叼！</p>
<h2 id="第二部分预告">第二部分预告<a href="#第二部分预告"><span class="icon icon-link"></span></a></h2>
<p>在这篇教程的第二部分，我们将学习使用 Webpack 加载 CSS 和 图片文件，同时让你的网站为部署做好准备。</p>
<h2 id="相关链接">相关链接<a href="#相关链接"><span class="icon icon-link"></span></a></h2>
<ol>
<li><a href="http://code.tutsplus.com/tutorials/introduction-to-webpack-part-1--cms-25791" rel="nofollow" target="_blank">Introduction to Webpack: Part 1</a></li>
<li><a href="https://segmentfault.com/a/1190000003970448" rel="nofollow" target="_blank">详解前端模块化工具-Webpack</a></li>
<li><a href="http://jamesknelson.com/webpack-made-simple-build-es6-less-with-autorefresh-in-26-lines/" rel="nofollow" target="_blank">Webpack Made Simple: Building ES6 &amp; LESS with autorefresh</a></li>
</ol>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/front-end">前端</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/introduction-to-webpack-part-1.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[不要总是走在准备的路上]]></title>
            <link>https://yufan.me/posts/just-try</link>
            <guid isPermaLink="false">https://yufan.me/posts/just-try</guid>
            <pubDate>Mon, 04 Apr 2016 04:45:43 GMT</pubDate>
            <description><![CDATA[上周和一个即将毕业的大学生聊天，他很厉害，学了很多，抱怨到很多人学了很多编程语言，但最后都只是会写一个“Hello World”的程度。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090419493263.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090419530721.jpg"><img src="https://cat.yufan.me/images/2020/09/2020090419493263.jpg?v=1777738726110" alt="9692 - 「中身が無い」" width="2048" height="1264" data-thumbhash="dwgGBIC495h3h2iIeop0bDD4FQ==">
<div class="tw:max-w-87.5 tw:mt-5 tw:mb-5.5 tw:max-xl:mx-auto tw:max-md:max-w-full tw:max-md:mt-0 tw:max-md:mx-0 tw:max-md:mb-5"><div class="aplayer" data-id="5047858"></div></div>
<p>上周和一位即将毕业的大学生聊天。他很厉害，学了很多东西，却感叹道：“很多人学了好多编程语言，但最后也只停留在能写一个 ‘Hello World’<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="true" aria-describedby="footnote-label">1</a></sup> 的程度。”
我笑了笑，说：“那是因为他们大多一直在<strong>准备</strong>学习，往往刚刚开始，就放弃了。”</p>
<p style="text-align:right">——题记</p>
<p>我叫雨帆。所有汉字中，我最喜欢、也最擅长写的就是“雨”和“帆”这两个字。并不是因为我字写得好，而是因为我专门练过。</p>
<p>这句话，其实可以套在很多事情上。无论是技能还是兴趣，想要掌握，都不是一蹴而就的，而是反复练习、持续积累的结果。没有人能一开始就成长为参天大树，即使是天才，也有常人难以想象的汗水与努力。</p>
<p>所以一直以来，我都很认同一句话：<a href="/posts/intimate-in-mind/">信念永远不能达到目的地</a>。每个人都有自己的梦想，但如果只是停留在“梦想”，不去行动，那么它也仅仅是一个梦。</p>
<p>记得三年前，有一位叫 <a href="/posts/meeting/">谢钦</a> 的网友给我发邮件，问了很多关于独立博客的问题。那一刻我就像在茫茫大海中看见了一束光。因为长期以来，我总以为只有我一个人在做这件事，身边无人理解。而突然有人懂你、与你共鸣的感觉，真的让人感动。</p>
<p>然而三年过去，他的博客主页上依然写着那句话：“未命名博客更新中……”</p>
<p>我并不是要刻意嘲讽这位朋友，而是觉得有些遗憾。因为，建立一个独立博客的准备过程真的很麻烦——尤其是在国内，还得经历繁琐的备案流程。若不是出于热爱，很多人一开始就会选择放弃。可惜的是，即便准备好了域名、服务器、系统、模板，却在最关键的一步——“开始写作”——上放弃了。</p>
<hr>
<p>写代码的时候，老大会经常提醒我们一句话：**不要过度设计。**所谓过度设计，就是和业务无关的东西写了一大堆，但真正关键的业务逻辑却很少。</p>
<p>说到这儿，不得不提知乎上那个很火的问题：<a href="https://www.zhihu.com/question/32039226/answer/76059969" rel="nofollow" target="_blank">为什么有些大公司技术弱爆了？</a>看到这个问题时，我就想起前老大常说的一句话——<strong>“满足业务需求的代码，就是好代码。”</strong></p>
<p>对于很多还没毕业的计算机专业学生来说，这句话可能难以理解。毕竟当他们看到一些外表光鲜的项目，却发现底层代码简直糟糕透顶，甚至数据库设计都违反教科书级规范时，往往会感到震惊。但事实是，这些项目之所以存在、盈利，正是因为它们最起码做对了一件事：<strong>满足了业务需求。</strong></p>
<p>当然，我并不是在为“烂代码”洗地。对于互联网公司而言，快速上线、持续迭代，才是维持竞争力的关键。尤其是智能手机用户可能最有体会——许多 APP 一周能更新好几次。那为什么不一开始就把设计定好呢？</p>
<p>从产品角度来看，**需求是不断变化的。**以输入法为例：最初用户只想打字时少选字，于是有了词库；后来又有了按词频排序的候选词；再后来，因网络聊天的普及，出现了 Emoji、字符画等功能……这些需求的出现，谁也无法在最初设计阶段预料到。如果你一开始就想面面俱到，潜心打磨几个月甚至几年，结果很可能是：要么市场早被别人抢走，要么你自己被耗死在设计阶段。</p>
<p>所以，很多事情一开始不可能就做到完美。如果只是一味思考“怎么做好”，而迟迟不动手，那永远也做不出来。你必须先动手——哪怕只是写个 demo，先跑通主流程，再一点点打磨细节。如果一开始就追求完美，要么累到想放弃，要么根本等不到成果。</p>
<hr>
<p>上周参加 OpenResty Meetup 时，我和一位架构师聊到“架构设计”。在他看来，<strong>架构的存在，是公司业务达到一定规模后的自然产物。<strong>对初创公司来说，架构往往不是必需品，因为业务量还没到那个程度。套用</strong>安迪·比尔定律</strong>，当你的业务规模还很小，再精致的架构设计都不如一个能跑的主流程。按照业内的常见标准，只有当业务量上升到百万级，才真正需要系统性的架构与设计。</p>
<p>当然，这种观点或许有些偏激，我保留意见。但如果回到“写代码”这件事上，我觉得成长路径大概是这样的：</p>
<blockquote>
<p><code>能写出代码</code> → <code>能写出满足业务需求的代码</code> → <code>能优化满足业务需求的代码</code> → <code>能为未来的需求变化预留接口</code> →<br>
<code>能将可变行为参数化</code> → <code>能在现有语言体系中持续演进</code> →<br>
<code>不再为设计模式而设计模式</code> → <code>理解并践行 Clean Code 准则</code> → <code>拥抱 TDD，不断重构与测试驱动。</code></p>
</blockquote>
<p>换句话说：
<strong>不要为了设计模式而设计模式，不要提前优化，不要过度封装，不要一味追新技术。</strong></p>
<p>很多经验丰富的程序员写的代码，往往小而精，恰到好处。当年我在写公司开放平台时，整整半个月几乎毫无进展。后来带我的师兄对我说了一句让我醍醐灌顶的话：</p>
<blockquote>
<p>“先理解需求，构建抽象实体，打通主流程。”</p>
</blockquote>
<p>如果我一直纠结“用什么框架”“怎么写才优雅”，那今天可能还没写出任何成果。</p>
<p>完美主义是很多人的通病。我们希望一开始就把事情做到最好，于是花大量时间准备、思考、权衡。等真正动手时，却发现别人早已领先几步。</p>
<p>当然，<strong>准备</strong>是必要的，但<strong>行动</strong>才是关键。只准备、不实践，再好的计划也只是纸上谈兵。</p>
<img src="https://cat.yufan.me/images/2020/09/2020090419530721.jpg?v=1777738726110" alt="9692 - bleu ciel" width="3156" height="2296" data-thumbhash="tecJFYSXl4h4d3iPeWWId0d5f5X3">
<section data-footnotes="true" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes<a href="#footnote-label"><span class="icon icon-link"></span></a></h2>
<ol>
<li id="user-content-fn-1">
<p>Hello World：基本上任何编程语言的教科书，一开始都是教大家如何写一个Hello World，久而久之成为一个梗。 <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/random-thoughts">随笔</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/just-try.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[工作心得总结]]></title>
            <link>https://yufan.me/posts/work-summary</link>
            <guid isPermaLink="false">https://yufan.me/posts/work-summary</guid>
            <pubDate>Wed, 23 Mar 2016 21:25:42 GMT</pubDate>
            <description><![CDATA[去任何一家公司都会遇到很多新鲜的知识，很多新鲜的内容，所以日常的学习过程中需要及时总结笔记，作为备忘。每个人都不可能纯凭记忆记住很多东西，所以日常的学习中，需要及时总结，也方便未来的梳理。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090420094090.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090420194343.jpg"><img src="https://cat.yufan.me/images/2020/09/2020090420094090.jpg?v=1777738726110" alt="456 - その白が" width="1318" height="988" data-thumbhash="L8cNHYSId3iPh3h6h5iHh3iPava4">
<h2 id="注意总结与反馈">注意总结与反馈<a href="#注意总结与反馈"><span class="icon icon-link"></span></a></h2>
<p>去任何一家公司都会遇到很多新鲜的知识，很多新鲜的内容，所以日常的学习过程中需要及时总结笔记，作为备忘。每个人都不可能纯凭记忆记住很多东西，所以日常的学习中，需要及时总结，也方便未来的梳理。</p>
<p>所谓的反馈，也就是展示自我，你要清楚的让上级知道你现在在做什么，会什么，有什么能力。也就是说，主动展示自我，主动秀出自己。通过这个，一来证明自己的能力，不是在那边混日子。二来也是一种资源的获取，上级懂的永远比你多，通过这种方式，可以要求到更多的学习资源。</p>
<h2 id="不断学习进步">不断学习进步<a href="#不断学习进步"><span class="icon icon-link"></span></a></h2>
<p>对于一个开发人员而言，你需要不断学习，因为这个世界上有着太多的牛人和技术。很多时候，你都会有瓶颈（自己的舒适区）。的确，现有的知识和能力足矣满足你的工作需求和未来的开发。</p>
<p>所以，要时刻惊醒，多去主动学习。</p>
<h2 id="问题处理与解决">问题处理与解决<a href="#问题处理与解决"><span class="icon icon-link"></span></a></h2>
<p>解决一个问题时，首先把上下文弄清楚。</p>
<p>对于负责解决问题的人要做到：</p>
<ol>
<li>明确问题提出的人是谁</li>
<li>确定问题描述清楚没有</li>
<li>及时将解决进度告诉大家</li>
</ol>
<p>如果问题解决了，要通过更新<code>Jira</code>或者在相关讨论组讨论的方式，告诉大家问题原因、解决办法、遗留问题。如果问题解决不了，或者提问者说OK了，一定要追问发生了什么，或者是如何解决的。</p>
<p>整个问题的生命周期都要与大家分享出来，不能不明不白的就关闭一个issue。</p>
<p>提问者需要及时追踪问题进度。同时，我认为：无论是任何原因，只要一个issue被长时间搁置，那么这个问题优先级一定不高。</p>
<img src="https://cat.yufan.me/images/2020/09/2020090420194343.jpg?v=1777738726110" alt="456 - あのね。" width="1054" height="782" data-thumbhash="eCgGDYDSa6d1WZnPMriIY/eWvW+q">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/work">工作</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/work-summary.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[基于 Kong 的 OAuth2.0 的使用]]></title>
            <link>https://yufan.me/posts/oauth-on-kong</link>
            <guid isPermaLink="false">https://yufan.me/posts/oauth-on-kong</guid>
            <pubDate>Thu, 04 Feb 2016 01:21:00 GMT</pubDate>
            <description><![CDATA[一个完整的 OAuth 2.0 认证需要好几步，然而，Kong只完成最后几步，实际使用时需要开发上层的服务。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2020/09/2020090420290983.jpg"><img src="https://cat.yufan.me/images/2020/09/2020090420290983.jpg?v=1777738726110" alt="もりちか - フレンドリーな猫に出会って、写真を撮りたくなっている女の子" width="1500" height="1062" data-thumbhash="ngcSDYKDhmefdnibh4eIdwadCfWo">
<h2 id="1-kong-oauth-20-认证流程">1. Kong OAuth 2.0 认证流程<a href="#1-kong-oauth-20-认证流程"><span class="icon icon-link"></span></a></h2>
<h3 id="11-相关角色">1.1 相关角色<a href="#11-相关角色"><span class="icon icon-link"></span></a></h3>
<ol>
<li><strong>第三方应用：</strong> 实际想要访问API的应用，需要在Kong上注册，定义自己的访问口令和回调地址。</li>
<li><strong>用户：</strong> 实际的第三方应用的使用者，和API访问权限的授权主体。</li>
<li><strong>认证服务：</strong> 基于Kong搭建的认证服务，主要完成用户信息的部分，也就是登录和授权提示页面。它作为第三方应用和Kong之间的中间桥梁，因为Kong不直接接管这些信息。</li>
<li><strong>Kong：</strong> Api Gateway，完整API的认证和应用注册等，将请求向后端转发。</li>
<li><strong>API 提供者：</strong> 实际的API提供者，与Kong交互，外部无法直接访问。</li>
</ol>
<h3 id="12-认证流程">1.2 认证流程<a href="#12-认证流程"><span class="icon icon-link"></span></a></h3>
<ol>
<li>Client为第三方应用，当它想访问某些需要用户授权的用户信息。</li>
<li>Client将用户跳转至 Kong 上的认证服务来认证应用。</li>
<li>如果认证服务发现用户未登录，那么将会跳转至登录界面。</li>
<li>在登录界面完成登录，将会跳转至认证服务的授权页面，显示第三方应用想要获取的用户信息，用户可以确定是否允许第三方应用访问。</li>
<li>用户确认，认证服务提交信息至Kong上完成注册认证流程，Kong返回认证口令和跳转地址给认证服务。</li>
<li>认证服务将用户跳转至 第三方应用 在 Kong 上定义的回调地址，传递认证口令。</li>
<li>第三方应用从回调地址读取认证口令，请求Kong，获取实际的信息访问口令。</li>
<li>应用通过每次请求传递附加上访问口令来调用API。</li>
</ol>
<h2 id="2-kong-oauth-20-问题开发需求">2. Kong OAuth 2.0 问题，开发需求<a href="#2-kong-oauth-20-问题开发需求"><span class="icon icon-link"></span></a></h2>
<p>一个完整的 OAuth 2.0 认证需要好几步，然而，Kong只完成最后几步，实际使用时需要开发上层的服务。</p>
<p>用户登录模块用户系统已经实现，登录方式为使用SDK传入Session ID获取用户登录信息，并不是很优雅。但是结合已有的登录服务，只需写一个登录界面，授权确认界面和与相关的交互服务。</p>
<h2 id="3-交互信息协议格式">3. 交互信息，协议，格式<a href="#3-交互信息协议格式"><span class="icon icon-link"></span></a></h2>
<h3 id="31-协议定义">3.1. 协议定义<a href="#31-协议定义"><span class="icon icon-link"></span></a></h3>
<p>（1）、新增 API</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># 请求</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http://127.0.0.1:8001/apis</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --data</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;name=httpbin&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;upstream_url=https://httpbin.org&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;request_host=yufan.me&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;preserve_host=true&quot;</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 响应</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;upstream_url&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;https://httpbin.org&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;b00e2c79-a87a-4688-b16f-7b2034171b13&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;created_at&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1456299654000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;preserve_host&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;httpbin&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;request_host&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;yufan.me&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（2）、API 配置 oAuth 认证</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http://127.0.0.1:8001/apis/b00e2c79-a87a-4688-b16f-7b2034171b13/plugins</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;name=oauth2&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;config.scopes=email,phone,address&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;config.mandatory_scope=true&quot;</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;api_id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;b00e2c79-a87a-4688-b16f-7b2034171b13&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;a21450ac-4aa6-459e-9678-21275fc15d6f&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;created_at&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1456299868000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;enabled&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;oauth2&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;config&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;mandatory_scope&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;token_expiration&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">7200</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;enable_implicit_grant&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;scopes&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;email&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;phone&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;address&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;hide_credentials&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;enable_password_grant&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;accept_http_if_already_terminated&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;provision_key&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;a36c23485d414ffb9eba7a85de0e7335&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;enable_client_credentials&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;enable_authorization_code&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（3）、添加开发者帐号</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http://127.0.0.1:8001/consumers</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;username=testuser123&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;custom_id=12345&quot;</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;custom_id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;12345&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;username&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;testuser123&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;created_at&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1456299997000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;364381e9-acf7-424d-a87f-f4be80871ee2&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（4）、添加应用</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http://127.0.0.1:8001/consumers/364381e9-acf7-424d-a87f-f4be80871ee2/oauth2</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;name=My%20Test%20Application&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;redirect_uri=https://httpbin.org/get&quot;</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;consumer_id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;364381e9-acf7-424d-a87f-f4be80871ee2&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;client_id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;54b968c73da64b328ed92b05548179b6&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;ce2906cb-3442-44c1-888e-848bafd0a442&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;My Test Application&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;created_at&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1456300207000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;redirect_uri&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;https://httpbin.org/get&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;client_secret&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;3b4537ac7c94492f81b251110e2d0f33&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（5）、模拟用户授权，获取回调码</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://127.0.0.1:8443/oauth2/authorize</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">-H </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;Host: yufan.me&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;client_id=54b968c73da64b328ed92b05548179b6&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;response_type=code&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;scope=email&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;authenticated_userid=yufan&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;provision_key=a36c23485d414ffb9eba7a85de0e7335&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--insecure</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{ </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">&quot;redirect_uri&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;https://httpbin.org/get?code=f2987a670ab246a38b2a3e6d58713019&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span></code></pre>
<p>（6）、获取两码，完成初次认证</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://127.0.0.1:8443/oauth2/token</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">-H </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;Host: yufan.me&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;client_id=54b968c73da64b328ed92b05548179b6&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;client_secret=3b4537ac7c94492f81b251110e2d0f33&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;grant_type=authorization_code&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;code=f2987a670ab246a38b2a3e6d58713019&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--insecure</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;refresh_token&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;a42472728fd74075ac8db82a0cb50b44&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;token_type&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;bearer&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;access_token&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;c28b23745aa84c14a001a32476be3d6c&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;expires_in&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">7200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（7）、API 请求</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://127.0.0.1:8443/post</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">-H </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;Host: yufan.me&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;access_token=c28b23745aa84c14a001a32476be3d6c&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--insecure</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;args&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {},</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;data&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;files&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {},</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;form&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;access_token&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;c28b23745aa84c14a001a32476be3d6c&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;headers&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;Accept&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;*/*&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;Content-Length&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;45&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;Content-Type&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;application/x-www-form-urlencoded&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;Host&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;yufan.me&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;User-Agent&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;curl/7.43.0&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;X-Authenticated-Scope&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;email&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;X-Authenticated-Userid&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;yufan&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;X-Consumer-Custom-Id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;12345&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;X-Consumer-Id&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;364381e9-acf7-424d-a87f-f4be80871ee2&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    &quot;X-Consumer-Username&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;testuser123&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;json&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;origin&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;127.0.0.1, 61.148.202.186&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;url&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;https://yufan.me/post&quot;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>（8）、API Token 刷新</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;m 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-bash"><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -X</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> POST</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://127.0.0.1:8443/oauth2/token</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">-H </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;Host: yufan.me&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;refresh_token=a42472728fd74075ac8db82a0cb50b44&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;client_id=54b968c73da64b328ed92b05548179b6&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;client_secret=3b4537ac7c94492f81b251110e2d0f33&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--data </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;grant_type=refresh_token&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">--insecure</span></span></code></pre>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-json"><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;refresh_token&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;de64c9df218b4a0eb2a24af83cd93a56&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;token_type&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;bearer&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;access_token&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;98ff79467dd6450f95061da94bd81d02&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  &quot;expires_in&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">7200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/kong">Kong</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/oauth-on-kong.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Rest API 的那些事儿]]></title>
            <link>https://yufan.me/posts/rest-api-design</link>
            <guid isPermaLink="false">https://yufan.me/posts/rest-api-design</guid>
            <pubDate>Tue, 08 Dec 2015 06:30:45 GMT</pubDate>
            <description><![CDATA[在软件行业快速发展的今天，传统的软件授权已经不能足以满足一个IT类的公司的发展。虽然在大部分公司里，它还是现金池的直接源头。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024041520451500.jpg"><img src="https://cat.yufan.me/images/2024/04/2024041520451500.jpg?v=1777731749579" alt="" width="1100" height="697" data-thumbhash="D8cJDIC/SFR4d4dAhYiwiLOdBg==">
<h1 id="一前言">一、前言<a href="#一前言"><span class="icon icon-link"></span></a></h1>
<p>在软件行业快速发展的今天，传统的软件授权已经不能足以满足一个 IT 类的公司的发展。虽然在大部分公司里，它还是现金池的直接源头。但是在可遇见的未来，受摩尔根理论的失效、物联网的发展等影响，应用的架构会越来越趋于简单化，架构越来越倾向于分布式水平扩展，对外的服务提供也会越来越 SaaS 化。在这种大背景下，很多公司都开始提供所谓的开放平台。</p>
<p>查阅各个大公司的开放平台，我们不难发现，都是 Rest API，都是 HTTP 请求，响应报文都是大同小异的 XML 或者是 JSON 等众多雷同的特点。这是为什么呢？让我们唠唠 API 平台的那些事。</p>
<h1 id="二定义">二、定义<a href="#二定义"><span class="icon icon-link"></span></a></h1>
<p><a href="https://www.ruanyifeng.com/blog/2011/09/restful.html" rel="nofollow" target="_blank">查看历史</a>，我们惊讶地发现，其实 Rest 的概念早在 2000 年就被人提出。用一句话描述它，就是<strong>用固定的 URI 和可变的参数访问某个服务，来完成一系列业务请求。</strong></p>
<ol>
<li>每一个 URI 代表一种资源；</li>
<li>客户端和服务器之间，传递这种资源的某种表现层；</li>
<li>客户端通过几个 HTTP 动词，对服务器端资源进行操作，实现“表现层状态转化”。</li>
</ol>
<h2 id="21-rest-api-格式">2.1 Rest API 格式<a href="#21-rest-api-格式"><span class="icon icon-link"></span></a></h2>
<p>Rest API，无论它的名字多么高大上，它本质还是一个 HTTP 请求，POST 也好，GET 也罢，都是不同的数据提交方式。所以，能够决定一个 Rest API 的也就：URI、参数、请求方式、请求头等。</p>
<p>我们一般用 <code>URI</code> 来定义希望对外暴露的服务。结构基本类似 <code>schema://yourCompanyDomain/rest/{version}/{application}/{someService}</code>。<code>schema</code> 可以是 <code>http</code>，也可以是 <code>https</code>，<code>version</code> 指的是你这个API的版本，<code>application</code> 一般会指向底层的某个子系统，<code>someService</code> 就是这个子系统对外提供的服务。当然，如果按照业务为边界划分，也可将业务维度相同但隶属于底层不同的系统的服务定义为一个 <code>application</code>。</p>
<p>对于这种Rest请求，常见的响应结果就是XML或者是JSON形式，往往结果中会包含请求状态，和时间戳，业务系统响应结果。</p>
<p>具体的格式约定，可以看底部的参考文献。</p>
<h3 id="211-api的版本概念">2.1.1 API的版本概念<a href="#211-api的版本概念"><span class="icon icon-link"></span></a></h3>
<p>在 <code>URI</code> 的格式定义中，我们包含了version这个字段，这在早期，其实被认为是不优雅的方式。阮一峰有<a href="https://www.ruanyifeng.com/blog/2011/09/restful.html" rel="nofollow" target="_blank">一篇文章</a>就专门抨击这种设计，后面他又自己打脸说还是拼接version的好。<em>（不知道是不是因为Github的设计缘故）</em></p>
<p><code>API</code>设计时常会考虑版本这个概念，无论是在URI还是在请求参数里面，至少有一个地方得指明含版本，为什么呢？</p>
<p>这很简单，就和系统迭代一样，API也是快速迭代开发的。也许初期，你的老大脑袋一热，我们要上 API，于是你们就加班加点做，设定了一版请求协议。当时你们想，写完就能赚钱，必然带来一堆问题，比如说，代码难以维护，功能单一。后面这个<code>API</code>的第二版，你们要基于它去做一些新的变更，比如请求参数多了，返回内容多了，必然就有了第二版。但是又不能影响现有的业务。这个 <code>API</code> 基本的 <code>URI</code> 没变，但是会同时存在两个版本，老用户继续请求旧版，没问题，你无需动旧版？有需求的新用户，你要请求新版才能提供服务。这样通过版本来区分了不同的服务，便于以后的升级和维护。<em>（就像 BAE 那货可以毫无压力地废弃掉 2.0 的API）</em></p>
<p><strong>所以一开始设计API时，就要定义好版本。其次，版本化可以骗钱。</strong> 我们可以满怀恶意地猜测，1.0为了抢用户，免费。2.0老牛逼了，你用的爽了，想更爽，付费。233333333</p>
<h2 id="22-架构特点">2.2 架构特点<a href="#22-架构特点"><span class="icon icon-link"></span></a></h2>
<p>API平台的架构，其实和底层公司的业务系统架构有着密切的关系，基于一个好的系统架构写一个Rest API平台基本是水到渠成的。我们先从业务系统的架构变迁说起，再来分析上面的API平台。</p>
<h3 id="221-业务系统架构变迁">2.2.1 业务系统架构变迁<a href="#221-业务系统架构变迁"><span class="icon icon-link"></span></a></h3>
<p>基本的软件公司的业务系统，一开始都是<code>单机，单节点，单库</code>。慢慢随着业务量的增加。这个系统越来越复杂，机器性能越来越不能满足需求。于是，第一种可能，领导说，换机器。上一台牛逼机器。但是机器性能有限，越牛逼的机器价格越贵，有时候都能买3~5台现有配置机器。于是就有了方案二，三个臭皮匠赛过一个诸葛亮。<code>单应用，多机器，多节点</code>。</p>
<p>但是慢慢过了几年。你发现，这代码写的越来越屎，越来越复杂。基本上新来的开发要熟悉好几个月的业务。就代码因为快速上线。一堆坑，无法改。于是，大家现在都在做的事情，就是拆分。也就是现在常说的<code>SOA</code>。拆分，也有拆的好的，拆的不好的。不好的，就是一个大的恶心系统，变成了一堆恶心的小系统，互相调用，成一团乱码。小系统看似很好。但是某个不起眼的小系统。一挂，那么全部的系统都瘫了。这个时候这个万年不维护的小系统还找不到负责人，他么早就滚了。这就是拆分的技术欠债，你无法避免。</p>
<p>那么，就谈到拆分的架构设计。其实这块分两个架构，<code>技术架构和业务架构</code>。技术架构，就是要分清<code>技术系统和业务系统</code>。技术系统也可能是一个业务系统，但它一定是一个通用的服务组件。它提供的服务无任何定制需求，就是纯简单服务。比如，发短信发邮件发微信的通知系统，它就是通知。你业务有何特殊需求，就在上面自己实现一个XXX通知系统，那么业务系统的拆分，才是最关键的。就是要**<code>划清边界</code>**。</p>
<blockquote>
<p>这个边界问题很可怕，什么你该做，什么你不该做。每个系统的职责都要明确。不要你也实现一个他也实现一个，然后相互调。<strong>这种可怕性就是在两个服务都瘫痪的时候，完全都无法启用</strong>。最后你的系统架构变成了一个通用技术组件系统，完成各个基础服务，每个产品线，业务端，基于你的技术系统包装出业务定制化服务系统，然后最上层就是业务子系统。业务子系统组合在一起，就是一个大的业务系统，也叫服务化平台。</p>
</blockquote>
<p><strong>这个时候，你需要做开放平台。暴露一套Restful API，就是水到渠成了。</strong></p>
<p>PS，实际的架构远比这个复杂，截图选自《大型网站系统与Java中间件实践》。</p>
<h3 id="222-基于不同系统架构的-api-平台">2.2.2 基于不同系统架构的 API 平台<a href="#222-基于不同系统架构的-api-平台"><span class="icon icon-link"></span></a></h3>
<p><strong>1、演变</strong></p>
<p>初期的 API 平台往往是上图左侧那种，某个庞大的业务系统希望暴露一套 API，于是大家就在这个系统上做，直接设计一套协议。但是，这样子带来的缺点十分明显，第一，它与业务联系太重，理想的API平台是通用的，不是只给你设计一个。第二，它不好扩展，每次变更都得和业务系统一同上线，糟糕的情况下代码还会影响原有正常的业务。第三，性能问题，理论上会降低原有应用的性能。</p>
<p>这种情况下，如果应用部署了多台机器，多个节点，我们就可以独立出来。也就是右边所示的 API Gateway，它做的事情本质上就是反向代理，将外部的请求校验完合法性之后反代至内部实际想要对外暴露服务的服务集群上。</p>
<p>所以，这种场景下，API Gateway 也就如名称所说，就是一个入口。实际的 Rest API 的东西还是建立在各个业务子系统上，只是只需要提供最简单的服务，无需考虑授权等东西。用户管理，API注册发布，调用统计等，均由 API Gateway 实现处理。对于想要快速上线的开发人员而言，实在是一个不错的福音。</p>
<p>然而，当系统应用拆分到了 SOA 化之后，API 的架构由有了新的变革，我们有了注册中心的概念。因为 SOA 化，所以每个业务子系统其实都有了对外的统一接口，有了 ESB（注册中心）。实际的内部系统间请求也有了较好的路由、熔断等策略。</p>
<p>在这种大背景下，API 平台对外暴露的 Rest API 无需底层的业务专门开发了。直接使用现有的内部接口，选择性暴露即可。问题点就在于，如何根据定义的Rest API请求，实际模拟内部的RPC协议请求。</p>
<p>某种程度上，这时候的API平台，已经不仅仅是 HTTP Rest 请求了。我们完全可以实现相同 RPC 协议的透传，比如你就是一个 Hessian 接口想对外暴露，我只需包上一层认证，直接注册于 API Gateway，外部 Hessian 请求直接透传至内部子系统。</p>
<p>在这个基础上的 Rest API 平台，才是灵活的，可扩展的，易于维护的。然而有得必有失，Mock 请求必然会有性能上的损耗，但是这个架构的公司，已经不在乎钱了，上 10 台虚机，不够加呗。</p>
<p><strong>2、特点</strong></p>
<ol>
<li>C/S 结构</li>
<li>无状态（API 平台无需存储业务状态，只做认证和转发）</li>
<li>有缓存，API 会对指定 URI 的请求转发做缓存，保证并发性，业务系统也对同样的请求针对性缓存。</li>
<li>结构分层，每层间无法直接访问。</li>
</ol>
<p>API 平台的背后，就是庞大的各个业务子系统。每个 API，就相当于一个业务子系统。API 平台要做的事，就非常清晰和简单。就是业务子系统注册发布 API，对外部请求校验计费，模拟请求内部业务子系统，对子系统结果包装序列化为 <code>JSON</code> 返回。</p>
<h2 id="23-交互流程">2.3 交互流程<a href="#23-交互流程"><span class="icon icon-link"></span></a></h2>
<p>上面是一个简单的交互，简单显示了外部系统和内部系统通过 Rest API 的交互过程：开发者（企业）注册，申请 APP_KEY，开通 API。按照开发接入，请求签名。转发至后端调用返回结果，API 平台计费（预付费或者后收费），统计调用情况。</p>
<p>外部通信本质上还是 <code>HTTP</code>，那么必然存在了授权问题，生产的 API 平台是直接暴露于公网的，如果认证授权策略出现纰漏，影响是可怕的。</p>
<p>比如，这个 API 是群发短信，你要是没有好的授权体系，允许人随意推送。某个人想搞你，调用发布反共信息，你整个公司都会跨。<code>HTTP</code> 协议本质上没有这一块的内容，所以我们必然要在这上面考虑安全策略的内容。</p>
<h3 id="231-如何保证-rest-api-的安全性">2.3.1 如何保证 Rest API 的安全性<a href="#231-如何保证-rest-api-的安全性"><span class="icon icon-link"></span></a></h3>
<p>如果单纯考虑加解密，或者签名方式来保证请求合法，其实是远远不够的。事实上，一个安全的 API 平台往往需要多方面一起考虑，保证请求安全合法。</p>
<p><strong>1、是不是实际客户端的请求？</strong></p>
<ol>
<li>设计专门的私有请求头：定义独有的 Request headers，标明有此请求头的请求合法。</li>
<li>请求包含请求时间：定义时间，防止中间拦截篡改，只对指定超时范围内（如 10 秒）的请求予以响应。</li>
<li>请求URI是否合法：此 URI 是否在 API 平台注册？防止伪造 URI 攻击</li>
<li>请求是否包含不允许的参数定义：请求此版本的这个 URI 是否允许某些字段，防止注入工具。</li>
<li>部分竞争资源是否包含调用时版本（Etag）：部分竞争资源，使用 If-Match 头提供。如用户资金账户查询 API，可以返回此时的账户版本，修改扣款时附加版本号（类似乐观锁设计）。</li>
</ol>
<p><strong>2、API 平台是否允许你调用（访问控制）？</strong></p>
<p>访问控制，主要是授权调用部分。API 都对外暴露，但是某些公共 API 可以直接请求，某些，需要授权请求。本质的目的，都是为了验证发起用户合法，且对用户能标识统计计费。</p>
<p>以 HMac Auth 为例，我们简单设计一个签名算法。开发者注册时获取 App Key、App Secret，然后申请部分 API 的访问权限，发起请求时：</p>
<ol>
<li>所有请求参数按第一个字符升序排序（先字母后数字），如第一个相同，则看第二个，依次顺延。</li>
<li>按请求参数名及参数值相互连接组成一个字符串。param1=value1&amp;param2=value2...（其中包含 App Key 参数）</li>
<li>将应用密钥分别添加到以上请求参数串的头部和尾部: secret + 请求参数字符串 + secret。</li>
<li>对该字符串进行 SHA1 运算，得到一个二进制数组。</li>
<li>将该二进制数组转换为十六进制的字符串，该字符串为此次请求的签名。</li>
<li>该签名值使用 sign 系统级参数一起和其它请求参数一起发送给 API 平台。</li>
</ol>
<p>服务端先验证<code>是不是实际客户端的请求</code>，然后按照 App Key 查找对应 App Secret，执行签名算法，比较签名是否一致。签名一致后查看此 App Key 对应的用户是否有访问此API的权限，有则放行。</p>
<p>执行成功后包装返回指定格式的结果，进行统计计费。</p>
<h1 id="三需求与实现">三、需求与实现<a href="#三需求与实现"><span class="icon icon-link"></span></a></h1>
<h2 id="31-需求">3.1 需求<a href="#31-需求"><span class="icon icon-link"></span></a></h2>
<h3 id="311-系统需求">3.1.1 系统需求<a href="#311-系统需求"><span class="icon icon-link"></span></a></h3>
<ol>
<li><!-- -->
<ul>
<li>安全认证</li>
<li>会话管理</li>
<li>流量统计及限流</li>
<li>计费收费</li>
<li>熔断</li>
</ul>
</li>
<li>支持现有子系统 RPC 协议的 API 动态发布及运营，外部请求透传。</li>
<li>支持 json、xml 响应报文，可以请求时选取所需报文格式。</li>
<li>支持动态直接将后端 SOA 服务暴露为 API。</li>
<li>支持动态将普通 Web 接口暴露为 API。</li>
<li>支持动态将 MQ 服务暴露为 API。</li>
<li>支持多个服务组合编排后暴露为 API。</li>
</ol>
<h3 id="312-业务需求">3.1.2 业务需求<a href="#312-业务需求"><span class="icon icon-link"></span></a></h3>
<p><strong>1、API 管理</strong></p>
<p>所有 API 可后台查询管理，包括动态发布、参数映射配置、后端服务接口配置、API 禁用、启用，多版本、分组、分级别等。</p>
<p><strong>2、应用管理</strong></p>
<p>后台管理开放平台接入的应用（第三方应用），包括查询、禁用、启用、审核。</p>
<p><strong>3、API 鉴权&amp;授权</strong></p>
<ol>
<li>应用申请审核通过后生成公钥，开放平台需提供支持分布式系统的密钥管理</li>
<li>服务可设置为两个安全等级：需授权访问和无需授权访问（后者即任意客户端都可以发起调用），默认所有API都需授权访问。</li>
<li>非正常状态（禁用、停用、黑名单等）的应用直接抛异常不允许访问——<strong>熔断机制</strong>
<ul>
<li>调用次数、调用频率、并发数可运行时控制，避免某请求量过大影响其他应用的调用。</li>
<li>可对某个应用某个 API 设置强制熔断，所有请求无视阀值直接抛出异常。</li>
</ul>
</li>
<li><!-- -->
<ul>
<li>与 SOA 集成，SOA 服务一键发布到API平台。</li>
<li>支持后台动态发布API，而不是新上一个API就需上线一次。</li>
</ul>
</li>
</ol>
<p><strong>4、计费统计</strong></p>
<ol>
<li>API的调用统计，每笔请求时间，响应时间，响应状态。</li>
<li>API的计费计算，按照请求量和请求资源计费，实现多种计费模型。（预付费，后收费。按量，按时间周期。）</li>
</ol>
<p><strong>5、开发者平台</strong></p>
<ol>
<li>API开发者平台，开发者注册、访问、申请API授权、计费统计、调用统计。</li>
<li>API文档系统，详细的API文档展示，SDK下载，用户登录后还可专门生成不同编程语言请求，在线模拟请求结果等。</li>
</ol>
<h3 id="312-角色定义">3.1.2 角色定义<a href="#312-角色定义"><span class="icon icon-link"></span></a></h3>
<p><strong>1、外部用户</strong></p>
<table><thead><tr><th>用户</th><th>做什么</th><th>使用目的</th></tr></thead><tbody><tr><td>API平台接入方</td><td>接入API平台</td><td>使用XXXX提供的开放平台服务</td></tr></tbody></table>
<p><strong>2、各个业务产品线</strong></p>
<table><thead><tr><th>用户</th><th>做什么</th><th>使用目的</th></tr></thead><tbody><tr><td>各个业务产品线</td><td>作为外部应用接入API平台</td><td>使用XXXX提供的开放平台服务</td></tr><tr><td>各个业务产品线</td><td>提供服务</td><td>提供后端服务，发布到API平台供外部应用接入</td></tr><tr><td>公司后端应用</td><td>提供服务</td><td>提供后端服务，发布到API平台供外部应用接入</td></tr><tr><td>API平台</td><td>API治理</td><td>运营，管理API、第三方应用等</td></tr></tbody></table>
<h2 id="32-请求模型">3.2 请求模型<a href="#32-请求模型"><span class="icon icon-link"></span></a></h2>
<p>API 的所有服务请求域名是相同的，区别在于Request Path等。请求参数分为系统级参数和业务级参数两部分，系统级参数是所有 API 都拥有的参数，而业务级参数由具体服务 API 定义。</p>
<h3 id="321-统一服务-url">3.2.1 统一服务 URL<a href="#321-统一服务-url"><span class="icon icon-link"></span></a></h3>
<p>建立API Gateway接受所有请求，按照Request Path，Request Method，Request Head分发所有的请求。</p>
<p><strong>1、 通用统一URL</strong></p>
<p><strong>格式</strong>：<code>schema://&lt;api Gateway URI&gt;/DispatcherServlet?method=XXService.xxMethod?xxxObj.xxxParam=xxxValue。</code></p>
<p><strong>说明</strong>：所有请求直接走<code>DispatcherServlet</code>分发，所有内容均定义于URL参数中。<code>method</code>为后端某个子系统的某个方法。<code>xxxObj.xxxParam</code>为方法参数实体的某个属性的值定义。</p>
<p><strong>示例</strong>：
<code>http://api.xxxxx.com/router?method=SMSService.sendSMS&amp;user.phoneNumber=18888888888&amp;sign=ds234324sdsad&amp;date=20151229231232</code></p>
<p><strong>2、Rest类型URL</strong></p>
<p><strong>格式</strong>：<code>schema://&lt;/api&gt;&lt;api Gateway URI&gt;/rest/{version}/{service}/{method}/{params}</code></p>
<p><strong>说明</strong>：请求按照Gateway定义的Rest地址匹配，动态映射至具体系统具体方法，模拟调用。请求中包含<code>version</code>字段。</p>
<p><strong>示例</strong>：
<code>http://api.xxxx.com/rest/v1/XXService/xxMethod/{xxParam}</code>
<code>http://api.xxxx.com/rest/v1/XXService/xxMethod?xxxParam=xxxValue</code></p>
<h3 id="322-参数设计">3.2.2 参数设计<a href="#322-参数设计"><span class="icon icon-link"></span></a></h3>
<p><strong>1、系统级参数</strong></p>
<p>系统级参数是由 API 平台定义的一组参数，每个服务都拥有这些参数，用以传送框架级的参数信息。如我们前面提到的 method 就是一个系统级参数，使用该参数指定服务的名称。</p>
<p><strong>2、业务级参数</strong></p>
<p>业务级参数，顾名思义是由业务逻辑需要自行定义的，每个服务 API 都可以定义若干个自己的业务级参数。API Getaway 根据参数名和请求属性名相等的契约，将业务级参数具体的方法请求对象中。</p>
<h2 id="33-常见框架">3.3 常见框架<a href="#33-常见框架"><span class="icon icon-link"></span></a></h2>
<ol>
<li>Kong: <a href="https://github.com/Mashape/kong" rel="nofollow" target="_blank">https://github.com/Mashape/kong</a></li>
<li>Zuul: <a href="https://github.com/Netflix/zuul" rel="nofollow" target="_blank">https://github.com/Netflix/zuul</a></li>
<li>ROP: <a href="https://github.com/itstamen/rop" rel="nofollow" target="_blank">https://github.com/itstamen/rop</a></li>
<li>Resty: <a href="https://github.com/Dreampie/Resty" rel="nofollow" target="_blank">https://github.com/Dreampie/Resty</a></li>
</ol>
<h1 id="四优劣">四、优劣<a href="#四优劣"><span class="icon icon-link"></span></a></h1>
<h2 id="41-好处">4.1 好处<a href="#41-好处"><span class="icon icon-link"></span></a></h2>
<ol>
<li>
<p><strong>跨平台</strong>，管你是<code>Java</code>，还是<code>PHP</code>，还是<code>Node.js</code>还是<code>Go</code>，你丫都得支持<code>HTTP</code>请求。我<code>API</code>平台只需要提供这个语言的<code>SDK</code>，保证能按照消息协议调用就好。</p>
</li>
<li>
<p><strong>将复杂的内部业务系统抽象为通用调用请求</strong>。包装了复杂的业务逻辑，对外提供统一的，好管理的接口。并可以定制化设计，计费，授权一类的容易管理。</p>
</li>
</ol>
<h2 id="42-坏处">4.2 坏处<a href="#42-坏处"><span class="icon icon-link"></span></a></h2>
<ol>
<li>
<p><strong>协议描述能力弱化</strong>，<code>Restful</code>的<code>URI</code>无法完全对请求参数做强格式校验。最后的方法参数绑定，模拟内部请求时往往容易出问题，尤其是以Java等强格式语言的系统。不能像<code>WebService</code>一样清晰描述请求报文。</p>
</li>
<li>
<p>同样的道理，响应结果为了是<code>JSON</code>、<code>XML</code>。这当中，编码，正反序列化，等操作，往往就会有性能瓶颈。而且，Java在这块资源消耗极大。以Github的<code>ROP</code>这个框架为例，当年测试时，它在并发请求过高的时候就会有一个内存泄漏问题。</p>
</li>
</ol>
<hr>
<h1 id="附参考资料">附：参考资料<a href="#附参考资料"><span class="icon icon-link"></span></a></h1>
<ol>
<li><a href="https://nirmata.com/2013/10/01/rest-apis-part-1/" rel="nofollow" target="_blank">REST Is Not About APIs, Part 1</a></li>
<li><a href="https://nirmata.com/2013/11/12/rest-apis-part-2/" rel="nofollow" target="_blank">REST Is Not About APIs, Part 2</a></li>
<li><a href="https://www.ruanyifeng.com/blog/2014/05/restful_api.html" rel="nofollow" target="_blank">RESTful API 设计指南</a></li>
<li><a href="https://www.ruanyifeng.com/blog/2011/09/restful.html" rel="nofollow" target="_blank">理解RESTful架构</a></li>
<li><a href="https://kb.cnblogs.com/page/521718/" rel="nofollow" target="_blank">撰写合格的REST API</a></li>
</ol>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/rest-api-design.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[如何更好地创建对象]]></title>
            <link>https://yufan.me/posts/the-better-way-to-make-objects</link>
            <guid isPermaLink="false">https://yufan.me/posts/the-better-way-to-make-objects</guid>
            <pubDate>Mon, 09 Mar 2015 05:27:15 GMT</pubDate>
            <description><![CDATA[写Java一类的面相对象语言的程序员常常会遇到这么一个冷笑话：我今年都30了，还没找到对象，怎么办？简单啊，new 一个对象就好。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024041520494100.jpg"><img src="https://cat.yufan.me/images/2024/04/2024041520494100.jpg?v=1777738726110" alt="Java" width="1400" height="733" data-thumbhash="GRUKFIYUW7eXh3iQW3g2d8B/1w==">
<p>写 Java 一类的面相对象语言的程序员常常会遇到这么一个冷笑话：我今年都30了，还没找到对象，怎么办？简单啊，new 一个对象就好。</p>
<p>当然这只是一个冷笑话，所谓的 new 一个对象，无非不就是调用这个类的构造方法去创建对象。乍一看也没什么问题，写个类 ABC，用的时候 <code>new ABC()</code> 就好了。那么，会想一下，我们读书的时候，老师一定会说一个类的构造方法可以允许传入参数，甚至根据传入参数的不同创建多个构造方法。</p>
<p>学过面向对象的你一定会说，没错啊，就是这样，方法重载，签名校验。都是一个方法，Perfect！那么，我们设想下面这么一种情况：</p>
<p>假如我们有一个 DTO 类 CredentialsAuthParam 作为对外接口的传入参数，作为一个 POJO 类，我们一般就是定义一堆属性，然后一堆 Getter、Setter，比如，我们可以这么定义：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">package</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> me.yufan.dto;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> org.apache.commons.lang3.builder.ToStringBuilder;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> org.apache.commons.lang3.builder.ToStringStyle;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> java.io.Serializable;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> implements</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Serializable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> final</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> long</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> serialVersionUID </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1L</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String source;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String validationCode;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String operator;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String remark;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getValidationCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> void</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setValidationCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">validationCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.validationCode </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getOperator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> operator;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> void</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setOperator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">operator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.operator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> operator;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getRemark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> remark;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> void</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setRemark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">remark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.remark </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> remark;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getSource</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> void</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setSource</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">source</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    @</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">Override</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ToStringBuilder.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">reflectionToString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                ToStringStyle.SHORT_PREFIX_STYLE);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>看着很棒，多么简单，用 CredentialsAuthParam 的时候 new 一下，作为传入参数调用接口的方法就好。</p>
<p>那么，如果我的对象要设置属性怎么办？一个个 Setter？为何不能在创建对象时创建呢？比如，构造方法里面指定参数？没错，然后我们就有了下面的一堆构造方法。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String source, String validationCode) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.validationCode </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String source, String validationCod, String operator) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.validationCode </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.operator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> operator;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String source, String validationCod,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                              String operator, String remark) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.validationCode </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.operator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> operator;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.remark </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> remark;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>那么问题来了，参数这么多，又都是 String，创建一个对象多麻烦。又比如，我只想指定其中 3 个属性，又都是 String，但现在已经有了一个为 3 个 String 参数的构造方法。怎么办？</p>
<p>Java 对方法重载的判断是按照签名的类型进行校验。所以方法参数顺序需要开发在使用时自己指定，也许我方法参数是：String source, String validationCode。但结果我因为复制粘贴不仔细变成了 <code>this.source = validationCode; this.validationCode = source;</code>（别笑，你忙着写垃圾代码的时候就会出错） 或者构造方法使用者弄混了两个参数的顺序。那么就会出事啦。</p>
<p>其实如果你看过 Java 圣经 Effective Java 的话，一定会注意到里面说过构造器（Builder），多个参数的构造方法一定要考虑使用构造器。比如，我们可以这么写：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">package</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> me.yufan.dto;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParamBuilder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String validationCode;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String operator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> String remark </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> &quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> CredentialsAuthParamBuilder </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setSource</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">source</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.source </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> source;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> CredentialsAuthParamBuilder </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setValidationCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">validationCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.validationCode </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> validationCode;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> CredentialsAuthParamBuilder </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setOperator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">operator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.operator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> operator;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> CredentialsAuthParamBuilder </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setRemark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(String </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">remark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.remark </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> remark;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> CredentialsAuthParam </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createCredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsAuthParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(source, validationCode, operator, remark);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>然后将原来类的构造方法定义为 Protected，然后创建对象的时候只需要：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CredentialsRequestParamBuilder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setRemark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">&quot;remark&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createCredentialsRequestParam</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span></code></pre>
<p>按照需求，设置几个属性就加几个 set 方法。</p>
<p>接下来，我们说说单例模式：</p>
<p>单例（Singleton），顾名思义，就是只被实例化一次的类。比如，我在 MVC 的拦截器中需要调用一个公共类，它里面存放的东西是大家共享的，我可以这么写：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> AuthorityInterceptorHelper</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> AuthorityInterceptorHelper instance;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    private</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> AuthorityInterceptorHelper</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> static</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> AuthorityInterceptorHelper </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getInstance</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (instance </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            synchronized</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (AuthorityInterceptorHelper.class) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (instance </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    instance </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> AuthorityInterceptorHelper</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> instance;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//    other code ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>我首先要在内部定义一个自身的静态对象，然后将构造方法私有，<code>getInstance()</code> 会先去看静态对象存在否，不存在，先加锁，也许加锁期间其他方法先调用此方法创建对象，再看看对象是否存在，不存在，创建对象解锁。</p>
<p>看起来没什么问题，代码严密，十分规范，大家都是这么写的。但是，单例了么？定义为私有的方法一定没法访问了么？反射呢？</p>
<p>其实枚举类便可以轻松实现需求，我们只需如下写法：</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" icon="&lt;svg viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/svg&gt;"><code class="language-java"><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">public</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> enum</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> AuthorityInterceptorHelper</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    INSTANCE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//    other function ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/java">Java</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/the-better-way-to-make-objects.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[我的Linux学习历程：那些我看过的Linux书籍们]]></title>
            <link>https://yufan.me/posts/this-is-the-linux</link>
            <guid isPermaLink="false">https://yufan.me/posts/this-is-the-linux</guid>
            <pubDate>Sat, 09 Aug 2014 05:24:49 GMT</pubDate>
            <description><![CDATA[来北京工作已经一个多月，大都市的生活比起读大学要忙碌得多，尤其是出行，基本以小时为计时单位。有时茫然看着窗外车水马龙，会有些迷茫自己选择的是对还是错。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2024/04/2024041520534000.jpg"><img src="https://cat.yufan.me/images/2024/04/2024041520534000.jpg?v=1777738726110" alt="Linux Books" width="800" height="450" data-thumbhash="3FcKJJBVRWdadXePinVVgHP3tw==">
<p>来北京工作已经一个多月，大都市的生活比起读大学要忙碌得多，尤其是出行，基本以小时为计时单位。有时茫然看着窗外车水马龙，会有些迷茫自己选择的是对还是错。</p>
<p>题外话不多说，回归这次的主题，扒一扒我看过的那些 Linux 相关的书籍。</p>
<p>对于 Linux 的了解和接触，缘起自大一时候的 Linux 限选课，老师说这门课可选可不选，但是选修后考试挂了的话需要补考。当时的我还比较爱学习，于是兴冲冲地选修了这门课，用的是清华大学出版社出版的《Ubuntu Linux 应用技术教程》。这本书写的并不是很好，里面对于图形界面的叫法还是最原始的 XWindows，此书一大半都是基于图形界面说什么安装啊、应用软件使用啊、七七八八的。但也简单讲了 <code>bash</code> 的使用和常见的命令，比如 <code>ls -all</code> 然后用管道 <code>|</code> 将输入导入至 <code>wc -l</code> 来计算文件数，还有一些简单的 vi 编辑器的使用，Shell 脚本的写法。</p>
<p>当时因为才开始学的原因，什么都不懂，渐渐地，也就失去兴趣没怎么好好学。课程对应的实验都是和老师卖萌才过的，考试的时候也就是背了一下往年的考题，考完就忘得一干二净。现在想起来，还有点小后悔。</p>
<p>本以为噩梦就此结束，我与 Linux 应该是老死不相往来，作为一名从小学四年级就开始学习使用 Windows 的用户，我真的很难适应和使用 Linux。可是大二学习操作系统原理的时候再一次无奈了，我们的一切实验的环境都是在 Linux 下面，使用那些基本 <code>fork()</code>、<code>pipe()</code> 等 Linux 下面的 C 函数进行操作系统的实验。当时在极其痛苦的情况下重拾课本，花了整整一周时间去学习使用 Shell，学习简单的 C 语言（<strong>妈的，看的是谭浩强的书</strong>），会用 vi 来编辑，会手动写 Makefile 使用 gcc 编译。</p>
<p>这期间通过学长的介绍知道鸟哥这位 Linux 大牛，开始阅读他的《鸟哥的 Linux 私房菜》。很遗憾的是，鸟哥的教程是基于 CentOS 来介绍的，看完开头极其冗余复杂的安装部分，我就“阵亡了”。“妈妈啊，快来救我，Linux 怎么这么复杂，什么 Ubuntu、什么 CentOS，还完全不怎么一样。”</p>
<p>真正意义上接触并使用 Linux，严格意义上说还得感谢“笨兔兔”这位 Linux 大牛。当时 Ubuntu 12.04 正好才出，本着不装逼就不会死的精神，我安装了这个系统，准备弄个双系统来装装逼。在研究如何配置 Ubuntu 12.04 的时候正好搜到了笨兔兔在他的博客发表的配置博文，当时也就是按照教程一行一行地复制粘贴命令。稍微知道如何使用 PPA 来安装 Ubuntu Tweak（一个国人写的对 Ubuntu 进行简单定制和优化的软件，你可以理解为 Win 里面的 360）。这期间阅读他的博客，接触到深度的 Linux 发行版 Deepin Linux。</p>
<p>什么？专门为国人打造的便于国人使用的 Linux？不错，装！</p>
<p>在我安装使用后立刻就被它的界面所吸引，坦白说 LinuxDeepin 12.04 真的很优秀，它基于 Ubuntu 12.04 的基础上，去掉了那个真的超级丑超级难用的 Unity，美化了各种常见的组件。直到现在，我家里上大学前的台式机还装的是此系统。</p>
<p>当时逛 Deepin 社区的时候，看到懒蜗牛 Gentoo 的《Linux 入门很简单》出版了，看完电子版前两章后我就被此书深深地吸引了。真的，它一点都不枯燥，语言十分浅显风趣，内容也简单实用。实体版到货后我立刻通宵两天把它看完，虽然最后部分的程序编译有点云里雾里，但是基本的日常使用已经无碍。</p>
<p>这个时候 Linux 已经基本被我用于日常的装逼，每当我在自习室前排用着 Linux 上网，常常会以一种看土鳖的眼神看着那些 Mac 上装 Windows 的同学，心里暗自发笑。</p>
<p>故事本该到了这里就结束，反正 Deepin 日常使用已经无碍，还有什么好学的。可命运就是这么神奇，它往往会在你最得意的时候给你浇盆冷水，让你“压压惊”。</p>
<p>那时已经到了 13 年，我开始迷恋上写博客，在学校组团参加 CCF 大会的时候接触到阿里云，和阿里云的销售一聊天，他大手一挥给了我一张半年的代金券，此券也开启了我新的 Linux 学习之路。</p>
<p>下单安装 ECS 系统，果断 Linux，选择系统的时候果断装逼，Ubuntu 和 Deepin 一样，咱不屑于使用，咱要用 CentOS。等云主机资源分配好可以运行时，我就傻眼了。纳呢，没有图形界面！只有一个简陋的 Shell 界面！这不是坑爹么！</p>
<p>没有熟悉的 apt-get，没有了熟悉的软件包名，我不得不重拾大一的课本继续蛋疼地看那些常见命令，yum 的使用也让我蛋碎一地。等我使用 yum 搭建好 LAMP 环境嘚瑟地在一个主机交流群里面炫耀时，某位大神（<strong>这里他要求不透露姓名</strong>）很是鄙视地说：现在流行 LNMP，流行编译源代码。</p>
<p>得，你丫瞧不起我是吧，我也给你整出来……</p>
<p>这期间学会了很多，参考了各种官方文档、手册、教程，一点点学会如何 <code>wget</code> 源码包，如何解包。看着 <code>./configure --help</code> 后面的一大串长到吐的参数一点点配置，看着 <code>make</code> 时候屏幕上滚动的各种 debug 信息暗自发呆。有时候为了解决一个简单的缺少依赖的问题 Google 一整个下午。即使是最后的编译完后，<code>make install</code> 之后的配置也让我头疼万分，Nginx 与 FastCGI 的交互，php-fpm 的配置，MySQL 的优化等等……</p>
<p>很庆幸有这么一段黑暗的学习经历，因为真正的动手学习配置才对 Linux 有了深入的了解。当然，用前女友云儿的话说：装逼是要付出代价的。因为对 Linux 本身产生了极大的兴趣，这期间也读了很多设计的书籍，比如那本《Linux/Unix 设计思想》。我至今依旧记得那个经典的小即是美的设计理念，没有代码层面的讲述，全书薄薄的一册介绍了各种 Linux 相关的编程理念，十分经典。</p>
<p>还有就是那本《Linux 内核设计与实现》，当时因为上选修课需要用到此书，在老师的讲授下结合以前学过的操作系统知识，真的能学到很多东西。比如，如何时间调度，如何实现排序，如何中断……</p>
<p>2013 年年底买了 Kindle，又一次开始了我的新的 Linux 学习之路，当时国行还没上市。因为学生党，兜里银子少，就买了日货。可是问题由此而来，日版没有中文界面。在研究时发现 Kindle 官网部分开源了 Kindle 的源码，于是我便萌发了自己编译 Kindle 内核来装逼的想法。从基础的交叉编译工具链的搭建，到内核源码的定制配置，BusyBox 的编译，UBoot 的定制，还有 Eink 驱动的移植……</p>
<p>因为涉及的东西很多很杂，在别人的指导下先看完了 LFS 手册，本着不装逼就不会死的原则先行尝试编译 LFS，然后慢慢上手嵌入式系统的编译。因为基础比较薄弱的问题（连 Shell 脚本都不会写），又买了一本《Unix&amp;Linux 大学教程》读完。</p>
<p>说了这么多废话，该讲讲今天参加图灵活动赠送的书籍 <strong>《Linux 就是这个范儿》</strong>。如果不是图灵市场部的英子姐姐在读者交流群里面公布了这个新书发布会，很有可能我就此与这本好书擦肩而过。这本书是由阿里巴巴的两位大牛 <strong>赵鑫磊</strong> 和 <strong>张洁</strong> 写的，赵老师一直在淘宝大学里面做 Linux 的培训，此书的前身就是他的上课讲义。</p>
<p>书的内容真的很全很全，但很遗憾</p>
<p>的是它不够细，可能是限于篇幅的原因，很多都只是简单介绍，一带而过。但是书籍的编写语言却十分风趣幽默，初看此书的第一眼，就有一种在看《Linux 入门很简单》的感觉，十分亲切。</p>
<p>整个图书发布会上，张洁老师的精彩演讲，赵老师的健谈和广泛的知识面都让人印象十分深刻。很多问题，从他们的口中都能得到很好地解答。我曾经一个 glibc 的依赖问题将整个系统 yum 仓库弄坏，和赵老师交流时竟然发现他也有相似的经历，当然结果是无解。（╮(╯▽╰)╭）</p>
<p>还记得赵老师在《Linux 就是这个范儿》一书开头的那个“黄色冷笑话”，简单回顾一下我这短暂的两年 Linux 学习历程。其实很容易发现，学习，就像爬山一样，没有平坦的大道，需要努力地去攀登，即使途中某段是平面，那是因为后面还有更加陡峭的阶梯等待着你去攀登。</p>
<p>学无止境！</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/turing">图灵</category>
            <category domain="https://yufan.me/tags/linux">Linux</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/this-is-the-linux.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[胡侃Android程序的效率]]></title>
            <link>https://yufan.me/posts/the-poor-efficiency-of-android-program</link>
            <guid isPermaLink="false">https://yufan.me/posts/the-poor-efficiency-of-android-program</guid>
            <pubDate>Mon, 25 Nov 2013 05:03:59 GMT</pubDate>
            <description><![CDATA[这天群里面大家都闲的时候，某君开始吐槽 Android 上的 UC 游览器，说以前版本的有多么快，现在的内存占用多大、多卡。一看到这个话题我就十分感兴趣，于是我就来胡扯一下 Android 上的程序效率问题，非专业文章，不喜勿看。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2013/11/2013112501353300.jpg"><img src="https://cat.yufan.me/images/2013/11/2013112501353300.jpg?v=1777738726110" alt="Broken Android" width="1280" height="768" data-thumbhash="X4cJHIRpSIeAhHmIhocIeXP2hw==">
<p>这天群里面大家都闲的时候，某君开始吐槽 Android 上的 UC 游览器，说以前版本的有多么快，现在的内存占用多大、多卡。一看到这个话题我就十分感兴趣，于是我就来胡扯一下 Android 上的程序效率问题，非专业文章，不喜勿看。</p>
<p>说起 Android 程序开发，可谓是又爱又恨。想想我们当年写对应的项目开发的时候，是有多复杂就往复杂里面写。后面实实在在接触了 CPP，然后会了析构，才发现 Java 的性能，就算发展 10 年，也赶不上 C、CPP，然后对于 UC 的内存占用和效率就释然了。</p>
<p>Android 程序用的是 Java，底层的 JVM（Java 虚拟机）其实是用 C 写的。╮(╯▽╰)╭ 一想到这个，我很多时候就非常想笑。用一个性能优秀一点的语言 Target 指定，然后解析一个性能差一点的语言的字节码，这也算是跨平台的硬伤吧，这就和 python 说自己比 C 还要快一样是个冷笑话。</p>
<p>而其实，解释一下跨平台和不跨平台的区别，你就能对于很多程序的性能就释然了。学过编译原理的可能会知道，一般指定平台的程序的编译流程是：词法分析-&gt;语法分析-&gt;语义分析-&gt;中间代码生成-&gt;中间代码优化-&gt;目标代码生成。</p>
<p>于是，程序在编译过程中会针对目标的平台进行相关优化，这样子的优点是程序的性能可能会更好，因为不同的平台可能说的指令集和中断调度算法都不一样，所以说编译程序时指定平台效率更高。（这里面细节我有兴趣时再说，涉及细节的计算机体系结构方面的知识。）</p>
<p>Java 一类的跨平台语言，比如还有 python，其本身的语法结构其实差不多，尤其在数据类型那边，基本一致。（因为编程语言的规范基本也就是 IEEE、MIT、ECMA 规定）它们的设计思想基本是编译到完整编译过程的中间代码部分，中间代码是平台无关的，一般是二进制文件。然后在通过平台相关的虚拟机运行中间代码。<strong>所以说 Java 是跨平台的，Java 虚拟机不是。或者说 Android 程序是跨平台的，但是 Android 系统不是。</strong>（正因为这一点，也导致了 Android 程序的反汇编很容易，破解也就自然不难了，目前大一点的 Android 程序都要混淆加密，虽然破解难度上升，但不是不能破解，只是运行效率不提也罢。）</p>
<p>由于硬件架构、硬件芯片不同。（也就是解决方案不同，MTK 是完整打包整板，但是高通不是）不同手机之间的 Android 系统要指定编译。尤其是在目前开发基本用交叉编译的时候，对应的虚拟机部分其实也是有区别优化的。所以，所谓的 Android 深度定制，其实本质就是对虚拟机进行指定平台相关的优化。</p>
<p>反过来继续说 Android 程序，因为为了跨平台，所以需要涉及虚拟机。一般的虚拟机都是用 C 写的，本身虚拟机也为了最大化兼容，已经丧失了一定的效率。然后再用这样子的低效率运行一个更加低效率的程序，这就是 Android 程序的本质无奈。</p>
<p>所以外界永远不知道的一个我们程序员的真相就是，我们永远是在为未来写代码。因为我们的代码永远不可能高效最优，一个软件写得越久，就会越来越冗余，越来越低效。然后我们就会把责任推给硬件，然后硬件就发展了。然后大家就一直买新的设备，我们就乐得写越来越低效的软件。</p>
<p>但是，这点在中国有点行不通。因为大家基本都是穷屌丝，一台手机不可能年抛，基本要用上个几年。所以才有了优化在中国的兴起。所以小米才会以优化定制为乐。但是这个的本质还是不行的，小米以后手机出得越多，架构越复杂的时候，它的优化也就越来越难做。而做 APP 开发的也很难做，一个是你的程序肯定会越做越复杂。目前的 Android 应用开发模式基本是 Scum 敏捷开发而不是以前的瀑布流，周期短，但是更新频繁。所以，你所面对的程序优化就越来越难做。</p>
<p>程序写复杂了，运行起来必定比以前慢，然后那群消费者就不干了。没错，新手机运行很流畅，很好，我们这群老用户呢？然后就一群人开始骂骂咧咧，然后应用站就开始刷新应用好卡、好慢、好烂，负分、差评、滚粗。而其实 Android 的开发门槛真的很低，做的基本是大专和本科，核心的优化有真的不行。所以垃圾应用铺天盖地都是，然后不赚钱，于是，有技术的也都懒得做了。</p>
<p>所以说，在天朝硬件想换代，难。所以逼着我们写垃圾代码，写低效率软件，然后逼着你换代升级。（微软的 Windows 就是一个典型的例子。）</p>]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/android">Android</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/the-poor-efficiency-of-android-program.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[大三暑期实训记]]></title>
            <link>https://yufan.me/posts/summer-training-record</link>
            <guid isPermaLink="false">https://yufan.me/posts/summer-training-record</guid>
            <pubDate>Thu, 18 Jul 2013 06:31:28 GMT</pubDate>
            <description><![CDATA[大概是很久没有提笔的缘故，一直找不到什么可以拿出来写的话题。七月的济南打破了一如既往的干旱，这一阵子阴雨连绵，恍然间有种已经回到厦门的错乱感。]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="https://cat.yufan.me/images/2013/07/2013071823450900.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2013/07/2013071823470800.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2013/07/2013071822320501.jpg"><link rel="preload" as="image" href="https://cat.yufan.me/images/2013/07/2013071823490400.jpg"><img src="https://cat.yufan.me/images/2013/07/2013071823450900.jpg?v=1777738726110" alt="LUNCH TIME - wacca〇わっか" width="1280" height="882" data-thumbhash="LMYFFYYTbmgwaopwuDiTiwJCAkN2">
<div class="tw:max-w-87.5 tw:mt-5 tw:mb-5.5 tw:max-xl:mx-auto tw:max-md:max-w-full tw:max-md:mt-0 tw:max-md:mx-0 tw:max-md:mb-5"><div class="aplayer" data-id="571336152"></div></div>
<p>大概是很久没有提笔的缘故，一直找不到什么可以拿出来写的话题。七月的济南打破了一如既往的干旱，这一阵子阴雨连绵，恍然间有种已经回到厦门的错乱感。只是现在的我依旧坐在实验室的机房内敲着代码，窗外下着大雨，身旁来自山东中医药的张远馨同学笑着对我说：“终于全部写完了。”</p>
<p><strong>“哎呀？已经结束了吗？”</strong></p>
<p>为期四周的项目实训终于在今天落下了帷幕，算起来，也好久没有像现在这样子忙碌了，早起晚睡、忙碌的学习和测试，实训中心里埋头敲代码的背影。恍然间，如同 Back To Seventeen。</p>
<p>有句话怎么说来的：纸上得来终觉浅，绝知此事要躬行。无论你学了多少，准备了多少，到了最后实施的时候总是觉得不够。只是，幸运的是我们做的只是项目实训，开学第一天和印度老师 Amit 交流时他就用英语和我们说：“这不到一个月的实训我的目的不是让你们学会多么深的技术，提高多少技术能力，主要是让你们在一种公司化的项目小组开发的气氛之下，严格按照公司开发步骤程序区完成这次的项目。”于是，公司化的管理，公司化的作息时间，公司化的小组协作开发……这一切的一切都是在之前自己的大学生活中并未体会过的。学习、设计、规划、编程、合并、测试，或许短短的流程线无法形象具体的表述出我们这二十多天的活动，但却可以隐约表示出自己在这实训期间充实的生活。</p>
<img src="https://cat.yufan.me/images/2013/07/2013071823470800.jpg?v=1777738726110" alt="インターネット世界の片隅で歌う - wacca〇わっか" width="1280" height="699" data-thumbhash="qaUFFIJV/pRqUZilaTV0j7RvAw==">
<p>因为自己之前对于 Android 项目的开发也只是处于入门阶段，并不熟练。所以初期两周自己先在网上找了一些 Android 项目开发视频、资料学习。在前几天的较为系统的学习过程中，我也掌握了很多在之后的开发过程中很实用的方法、技巧。期间、自己也编写了部分小程序、范例进行熟悉。</p>
<p>而其实自己在做具体功能模块实现部分时花了不少的时间，其中遇到了很多复杂的问题。怎样在界面里实现各功能的代码编写，怎样实现 SQLite 数据库的初始化、更新、增删改查，怎样在各个 Activity 间相互传递参数等等。而这些又都是比较棘手的问题，又比如：利用 Intent 传递参数时，无法直接传递已经构建好的实体类参数；利用 SimpleAdapter 时，无法在 ListView 实现 Item 按钮的添加、绑定。但是通过向老师 Amit、同学请教；通过组员的帮助和一次又一次的调试，这些问题都解决了。直到这时我才深深的体会到，团队合作的力量是多么的巨大。正是因为有了老师和同学们的帮助，我的实训任务才可以提前并且很好的完成。</p>
<img src="https://cat.yufan.me/images/2013/07/2013071822320501.jpg?v=1777738726110" alt="代码片段" width="1366" height="727" data-thumbhash="+gcGBICkSYiBeHeAd3H6eHD35w==">
<p>而代码整合中存在的问题，应该算是我的实训过程中最有意义的收获之一了。在此过程中，实际的教训使我再一次意识到评审的重要性。由于开发周期较短，需要每天进行一次评审，而我之前的评审时间有点晚，导致发现问题不够及时，因此最后拖慢了大家的进度。</p>
<p>最后，要感谢 Amit 师对我们的耐心讲解和帮助，还有我们组组长和小组成员对我的热心帮助，他们都给了我前进的动力，教给了我许多知识和课本以外的东西，没有他们的帮助，我就很难完成组长分配给我的任务了，没有他们的讲解，我也不会学到那么多的东西，有那么多的收获，所以也借此文章偷偷地感谢他们（╮(╯▽╰)╭）。</p>
<p>人非生而知之，虽然我现在拥有一定的知识结构，但是我知道自己需要锻炼动手能力，不仅要靠努力学习，还要靠潜心实践。没有实践，学习就是无源之水，无本之木。这次实训让我知道了不少知识：我们不可能永远呆在象牙塔中，过着一种无忧无虑的生活，我们总是要走上社会的，而社会，就是要靠我们这些年轻的一代来推动。不久后的我，面临是就业压力，我想我们都应该好好经营自己的时间，充实、完善自我，不要让自己的人生留下任何空白！相信努力就会有收获，每天努力充实自己，相信在不久的将来，可以打造一片属于自己的天地。</p>
<p>“既然选择了远方，便只顾风雨兼程！”</p>
<p>写完实训报告时望着窗外，天空已然阵雨初晴。</p>
<img src="https://cat.yufan.me/images/2013/07/2013071823490400.jpg?v=1777738726110" alt="忘れないで - 三月（みつき）" width="1280" height="874" data-thumbhash="atgJDYL5tpd7hnmJhWh3jAx76cEH">]]></content:encoded>
            <author>syhily@gmail.com (雨帆)</author>
            <category domain="https://yufan.me/tags/practical-training">实训</category>
            <category domain="https://yufan.me/cats/coding">编程</category>
            <enclosure url="https://yufan.me/images/og/summer-training-record.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>